前几天跟一个解释代码,他怎么都理解不了我写的使用overload转换QOverload的代码。最后我只能跟他说你就记住我是这么写的,你在上面改就是了。今天有时间,把这个东西详细地写一下。
C++17中的overload
C++17中提供了overload模式,在std::visit的示例中。不注意还真的注意不到。
1 2 3 4 5 6 7 8
| template <typename... Ts> struct overload : Ts... { using Ts::operator()...; };
template <typename ... Ts> overload(Ts...) -> overload<Ts...>;
|
按照上面的观点,它应该是作为一个visitor和std::visit
配合使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void overload_test() { auto twice = overload{ [](std::string v) { std::cout << "twice string: " << v+v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; }, };
using MultiVal = std::variant<int, std::string>;
std::cout << "和visit配合使用的用法\n"; std::visit(twice, MultiVal(1)); std::visit(twice, MultiVal("hellow"));
std::cout << "把overload当作可调用对象直接使用:\n"; twice(12); twice("world");
std::cout << "直接定义临时对象并使用\n"; overload{ [](std::string v) { std::cout << "twice string: " << v + v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; }, }(12);
std::cout << "直接在visit中使用:\n"; std::visit(overload{ [](std::string v) { std::cout << "twice string: " << v + v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; }, }, MultiVal("goodbye")); }
|
当然,我们也可以不用std::visit
,可以直接用overload
——它就是一个callable object。
1 2
| twice(12); twice("world");
|
当然,我们也可以不创建twice
这个对象实例,直接在std::visist
中定义overload
,甚至直接定义overload
并调用它,都可以。
后面本质上是创建了一个临时对象而已。
我们真正关心的是overload
这个东西,正如前面看到的,它不仅仅是可以用于std::visit
。
我们先跳过支持任意类型,只看一个要支持两种类型:int
和std::string
的一个overload的东西怎么实现。
最古老的实现
在最古老的C++时代,比C++98或更老的C++,我们要写一个可以处理多种类型的callable object,最普通的是这种实现:
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct VeryOldOverload { void operator()(int v) { std::cout << "twice int value: " << 2 * v << std::endl; } void operator()(std::string v) { std::cout << "twice string value: " << v + v << std::endl; } };
void veryOldOverload_test() { VeryOldOverload a1; std::cout << __func__ << std::endl; a1(11); a1("hellow"); }
|
利用继承实现
进一步,利用继承实现这个类,这样,我们可以把数据处理逻辑给提取出来共享:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| struct IntWraper { void operator()(int v) { std::cout << "twice int value: " << 2 * v << std::endl; } };
struct StringWraper { void operator()(std::string v) { std::cout << "twice string value: " << v + v << std::endl; } };
struct DoubleWrapter { void operator()(double v) { std::cout << "power double value: " << v * v << std::endl; } };
struct DerivedOverload : public IntWraper, StringWraper { using IntWraper::operator(); using StringWraper::operator(); };
void TemplateOverload_test() { DerivedOverload a; a("hell"); a(120); }
|
其中,在DerivedOverload
中的using IntWraper::operator()
是将IntWrapter::operator()
放到DerivedOverload
的命名空间下面。这个using用法是什么时候的我已经不记得了(好像是C++98才有的)
但是,这个问题在于,我们定义了DerivedOverload
,它能够支持的类就定死了。如果我们还需要一个支持double
的怎么办?我们就需要再顶一个新的overload类。所以,才会有模板技术。
利用模板支持
接下来,我们考虑使用模板生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| struct IntWraper { void operator()(int v) { std::cout << "twice int value: " << 2 * v << std::endl; } };
struct StringWraper { void operator()(std::string v) { std::cout << "twice string value: " << v + v << std::endl; } };
struct DoubleWrapter { void operator()(double v) { std::cout << "power double value: " << v * v << std::endl; } };
template <typename T1, typename T2> struct TemplateOverload : public T1, T2 { using T1::operator(); using T2::operator(); };
void TemplateOverload_test() { TemplateOverload<IntWraper, StringWraper> t; t("Hellow"); t(123); }
|
使用模板的改进不大,它只能支持两个类,如果要支持一个,三个怎么办?我们还是需要再定义一个类的overload类,三个类的overload。所以,C++11引入了变参模板。
任意基类的模板
接下来就是如何支持任意的基类了。这个需要使用变参模板。我们先看C++17之前该怎么实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| template<typename T1, typename... Ts> struct VarOverload : T1, VarOverload<Ts...> { using T1::operator(); using VarOverload<Ts...>::operator(); };
template<typename T> struct VarOverload<T> : public T { using T::operator(); };
void VarOverload_test() { VarOverload<IntWraper, StringWraper> a; a(12); a("Hellow world"); }
|
从C++11开始支持模板变参,在C++17之前,我们必须使用递归的形式来定义,并且要注意,必须将递归推出条件写在后面而不是前面(至少MSVC编译器是这样要求,我没有仔细看过规范)。
其实我觉得递归形式更好理解:-)。也许是我用得还不够多,不像递归,还没有形成直觉😊
变参折叠和单行using支持的实现
在C++17种,引入了两个对这个问题很重要的特性:
第一个是模板变参的折叠,第二个是支持一行中多个逗号隔开的using。在这里,就是这种写法成为可能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <typename... Ts> struct FoldOverload : Ts... { using Ts::operator()...; };
void FoldOverload_test() {
FoldOverload<IntWraper, StringWraper, DoubleWrapter> a2; a2(22); a2(std::string("Hellow")); a2(2.0); }
|
它实际上将FoldOverload<IntWraper, StringWrapter, DoubleWrapter>
展开后得到:
1 2 3 4
| struct FoldOverload : IntWraper, StringWrapter, DoubleWrapter { using IntWraper::operator(), StringWrapter::operator(), DoubleWrapter::operator(); };
|
而在我们前面的递归版本中的最终是:
1 2 3 4 5 6
| struct FoldOverload : IntWraper, StringWrapter, DoubleWrapter { using IntWraper::operator(); using StringWrapter::operator(); using DoubleWrapter::operator(); };
|
主要的差别 就是using
是否在一行中。
现在,我们已经可以很容易做一个overload了。但是和前面的形式还是有很大的区别的。这是怎么回事?
C++对初始化列表的扩展支持
从C++11开始,支持大括号形式的初始化列表。例如,考虑下面的代码:
1 2 3 4 5 6 7 8 9 10 11
| struct B { int b1; int b2; };
void test_initialize() { B b{ 1,2 }; std::cout << "b::b1=" << b.b1 << ", b::b2=" << b.b2 << std::endl; }
|
但是,如果我们从B
派生出类D
,就不行了:
1 2 3 4 5 6 7 8 9 10 11 12
| struct D : B { int vd; }; void test_initialize() { B b{ 1,2 }; D d{ 1,2,3 }; std::cout << "b::b1=" << b.b1 << ", b::b2=" << b.b2 << std::endl; std::cout << "d::b1=" << d.b1 << ", d::b2=" << d.b2 << ",d::vd=" << d.vd; }
|
将编译器标准设置为C++14,编译下面的代码,编译器会报错:error C2440: “初始化”: 无法从“initializer list”转换为“D”
.
我们改成D d{ {1,2},3 };
一样也不行。
将C++标准改为C++17,则上面的代码能编译通过。
这种使用方式,在C++中的约束是:
- 无用户定义的构造函数
- 无private或protected的非静态数据成员
- 没有虚函数
- 没有基类
C++17将最后一条限制去掉了。通过这个,就为通过大括号直接初始化一个类扫平了道路。
类模板参数推导
在C++17之前,当使用模板时,必须指定模板类的参数。例如,
1 2
| std::mutex _mutex; std::lock_guard<std::mutex> lock{_mutex};
|
在C++17之后,可以根据实际参数推导出模板参数,从而,上面的代码可以写成这样:
1 2 3 4
| std::mutex _mutex; std::lock_guard lock{_mutex}; std::vector lst{1,2,3}; std::vector lst2{"hellow", "world"};
|
这样可以让我们在声明类的时候,不用再将模板参数名称写一遍。
在C++17之前,因为不支持类模板参数的推导,我们需要借助函数参数推导来进行推导。例如,常用的std::make_pair()
函数,就是在无法直接使用std::pair
做推导的时候提供的:
1 2 3
| std::pair<double, int> m1{2.5, 5}; std::pair m2{2.4, 5}; auto m3 = std::make_pair(2.4, 5);
|
这个有啥用呢?它有助于我们推导出Lambda的类型。比如,下面的类,封装了一个回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| template <typename CB> class CountCalls { private: CB _callback; long _call_count{ 0 }; public: CountCalls(CB cb) : _callback{ cb } {};
template<typename... Args> auto operator() (Args&&... args) { ++_call_count; return _callback(std::forward<Args>(args)...); }
long callCount() const { return _call_count; } };
void callCount_tester() { CountCalls sc([](auto x) { std::cout << "call once: " << x << std::endl; });
sc(1); cout << "call count is: " << sc.callCount(); sc(2); cout << "call count is: " << sc.callCount(); }
|
这个类和我们在前面的overload
原理是一样的,只不过它只有一个模板参数。
在C++17之前,这个是编译不过的,我们需要定义一个辅助模板函数来返回一个CountCalls
对象使用。在C++17中就不需要了。我们可以把编译器版本改为C++14,就会发现编译不过了。
我们需要写一个辅助函数:
1 2 3 4
| template <typename T> constexpr auto make_callcount(T&& t) { return CountCalls<T>{std::forward<T>(t)}; }
|
然后:
1 2 3 4 5 6 7 8 9
| void callCount_tester() { auto sc = make_callcount([](auto x) { std::cout << "call once: " << x << std::endl; }); sc(1); cout << "call count is: " << sc.callCount(); sc(2); cout << "call count is: " << sc.callCount(); }
|
这样就可以了。
注意,这里的constexpr
并不是必须的。
最后,对于前面的FoldOverload
,如果想要编译成功,也需要一个辅助函数;我们重新完整的写一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| template <typename... Ts> struct FoldOverload : Ts... { using Ts::operator()...; };
template <typename ...Ts> constexpr auto make_foldoverload(Ts&&... args) { return FoldOverload<Ts...>{std::forward<Ts>(args)...}; }
void FoldOverload_test() { auto s = make_foldoverload( [](std::string v) { std::cout << "twice string: " << v + v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; } ); }
|
我们看到,这个make
函数其实纯粹是千篇一律的脚手架代码,而C++17于是又提供了一个新特性,叫做Deduction Guide,使得我们可以免去写这个函数,而是只需要指明它的推导规则就可以。
我们将Deduction Guid
和make函数都写在一起,比较一下它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| template <typename... Ts> struct FoldOverload : Ts... { using Ts::operator()...; };
template <typename ...Ts> constexpr auto make_foldoverload(Ts&&... args) { return FoldOverload<Ts...>{std::forward<Ts>(args)...}; }
template<typename ...Ts> FoldOverload(Ts...) -> FoldOverload<Ts...>;
void FoldOverload_test() { auto o1 = make_foldoverload( [](std::string v) { std::cout << "twice string: " << v + v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; } ); auto o2 = FoldOverload{ [](std::string v) { std::cout << "twice string: " << v + v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; } }; }
|
C++20的overload
到了C++20,类模块的推导规则又得到了进一步的增强,现在连推导规则也不需要了。只要这样写就够了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template <typename... Ts> struct FoldOverload : Ts... { using Ts::operator()...; };
void FoldOverload_test() { auto o2 = FoldOverload{ [](std::string v) { std::cout << "twice string: " << v + v << std::endl; }, [](int v) { std::cout << "twice int value: " << 2 * v << std::endl; } }; ... }
|
至于说这个东西到底有没有价值?我也不好说,这种技巧不是必须的。C++很多的特性更多的可能只是炫技,人生艰难,从中找点乐趣罢了。