Daily C/C++ output_container解析
这篇文章是来解析一下吴咏炜老师的output_container
我这里也给出我的一个例子,用来帮助理解
int main() {
std::pair p{1, 2};
std::cout << p << std::endl;
std::vector vec{1, 2, 3, 4, 5};
std::cout << vec << std::endl;
std::unordered_map<int, int> mp = {
{1, 2},
{3, 4},
{5, 6}
};
std::cout << mp << std::endl;
std::vector<std::pair<int, int>> vec2{{1, 2}, {3, 4}, {5, 6}};
std::cout << vec2 << std::endl;
return 0;
}
就是正常的输出,使用g++ xxx.cpp output_container.h --std=c++17
即可完成编译
输出结果如下
(1, 2)
{ 1, 2, 3, 4, 5 }
{ 5 => 6, 3 => 4, 1 => 2 }
{ (1, 2), (3, 4), (5, 6) }
另外提一嘴,专栏文章中已经有解析了,这里只不过是加上个人的理解后的一个补充
那么我们正式开始,首先可以知道,这个代码的作用是帮助我们重载对容器的<<
运算符,使得我们可以直接输出这个容器里的内容
那么我们最常用的容器我上面已经有提到,就是vector
,map
,pair
等
而map里面储存的键值对也是以pair作为单元的,所以思路就是在输出的时候来区分平凡数据类型POD和pair
所以最开始这段代码
template <typename T>
struct is_pair : std::false_type {};
template <typename T, typename U>
struct is_pair<std::pair<T, U>> : std::true_type {};
template <typename T>
inline constexpr bool is_pair_v = is_pair<T>::value;
用来让我们区分类型T是不是pair类型
然后下面的代码表示检测这个类型是不是已经有重载了输出运算符了
template <typename T>
struct has_output_function {
template <class U>
static auto output(U* ptr)
-> decltype(std::declval<std::ostream&>() << *ptr,
std::true_type());
template <class U>
static std::false_type output(...);
static constexpr bool value =
decltype(output<T>(nullptr))::value;
};
template <typename T>
inline constexpr bool has_output_function_v =
has_output_function<T>::value;
作用就是如果这个容器已经有输出运算符,我们再给一个,就会出现两个可用的函数,就会报错
// Output function for std::pair
template <typename T, typename U>
std::ostream& operator<<(std::ostream& os, const std::pair<T, U>& pr);
// Element output function for containers that define a key_type and
// have its value type as std::pair
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element,
const Cont&, const std::true_type)
-> decltype(std::declval<typename Cont::key_type>(), os);
// Element output function for other containers
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element,
const Cont&, ...)
-> decltype(os);
然后这里是声明输出的部分,可以看到我们单独重载了对pair的输出,我们可以看到实现
然后我们还有两种用于输出元素的函数,第一种可以看到,是用来处理pair类型,同时这个容器包含有key_type类型的情况。其实就是映射的情况,我们有对应的key和value
template <typename T,
typename = std::enable_if_t<!has_output_function_v<T>>>
auto operator<<(std::ostream& os, const T& container)
-> decltype(container.begin(), container.end(), os)
{
using std::decay_t;
using std::is_same_v;
using element_type = decay_t<decltype(*container.begin())>;
constexpr bool is_char_v = is_same_v<element_type, char>;
if constexpr (!is_char_v) {
os << "{ ";
}
if (!container.empty()) {
auto end = container.end();
bool on_first_element = true;
for (auto it = container.begin(); it != end; ++it) {
if constexpr (is_char_v) {
if (*it == '\0') {
break;
}
}
if constexpr (!is_char_v) {
if (!on_first_element) {
os << ", ";
} else {
on_first_element = false;
}
}
output_element(os, *it, container, is_pair<element_type>());
}
}
if constexpr (!is_char_v) {
os << " }";
}
return os;
}
然后看这里的实现,这个就是我们重载的主体,他会首先用decay得到对应容器的值类型,然后来判断char
这里char的单独处理就不多赘述,我们直接看下面,我们用迭代器遍历每一个元素,然后有两个编译期if可以帮助我们简化代码
注意这里的if是编译期的,因为我们是在编译期实例化了对应的模板,也就得到了对应的element_type。在编译期is_char_v要么为真,要么为假
然后我们对每一个元素调用对应的output_element
注意这里虽然我们构造了is_pair这个结构体,但是实际上我们只是需要他的类型来匹配,实际的参数并不需要他
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element,
const Cont&, const std::true_type)
-> decltype(std::declval<typename Cont::key_type>(), os)
{
os << element.first << " => " << element.second;
return os;
}
template <typename T, typename Cont>
auto output_element(std::ostream& os, const T& element,
const Cont&, ...)
-> decltype(os)
{
os << element;
return os;
}
template <typename T, typename U>
std::ostream& operator<<(std::ostream& os, const std::pair<T, U>& pr)
{
os << '(' << pr.first << ", " << pr.second << ')';
return os;
}
再看下面的输出就容易了,这里就是对于键值对类型的,我们会以x => y
的形式输出,否则我们就调用普通的output_element
对于普通的版本,还有pair需要处理,所以我们单独为pair重载<<
运算符。因为返回值不能来区分重载,所以我们的pair也就无法和键值对分开,所以就使用了这种方法
还有一个要说的点是这里
template <typename T,
typename = std::enable_if_t<!has_output_function_v<T>>>
auto operator<<(std::ostream& os, const T& container)
这里的SFINAE的使用方法,我们是提供了默认的模板参数,这里再通过enable_if_t来判断。
因为当里面的值为false的时候,enable_if是没有type的,所以就会匹配失败
这样的做法有一个小缺陷,就是他是不能处理多个条件的情况的
详见cppreference中
第一种做法被认为是同一个函数模板的再声明,所以更好的做法是使用下面的,改变模板参数
当然,对于这个代码,我们只用了这一种情况是没什么问题的。
差不多就这些
巧夺天工,看来学习C++的路还很长
文章评论