C++ 每三年才解决一点点问题
或:怎样优雅地给 C++ 模板添加约束?
我们有一个基类 Base
,一个子类 Derived: public Base
,和一个无关的类 Other
。我们希望实现一个类模版 M<T>
,只能接受该类的派生类作为类型参数,即 M<Derived>
可以编译通过,而 M<Other>
则拒绝编译。应该怎么办?
以下代码皆依赖头文件
<type_traits>
实现
C++ 98
必须从头开始造轮子。此处略。
C++ 11
template<typename T,
typename =
typename std::enable_if<std::is_base_of<Base, T>::value>::type>
struct C {};
C++ 14
C++14 支持 std::enable_if_t
了,这下代码变短了一点。
template<typename T,
typename = std::enable_if_t<std::is_base_of<Base, T>::value>>
struct B {};
C++ 17
借助 std::is_base_of_v
,我们终于可以在 80 个字符的宽度限制下把 template
声明写在一行里了。
template<typename T, typename = std::enable_if_t<std::is_base_of_v<Base, T>>>
struct C {};
C++ 20
2020年3月24日 Clang 10.0 发布。终于我们有了全功能的 Concept。1 随后 GCC 10.0 也支持了 Concept。
template<typename T> requires std::is_base_of_v<Base, T>
struct D {};
目前所有 C++ 编译器中只有 GCC 实现了 <concept>
头文件,在 GCC 10.0 中代码可以更加简化:
template<std::derived_from<Base> T>
struct E {};
错误信息
C++ 11
GCC Output
❯ g++ -c a.cpp -std=c++2a
a.cpp: 在函数‘int main()’中:
a.cpp:36:12: 错误:no type named ‘type’ in ‘struct std::enable_if<false, void>’
36 | A<Other> d1;
| ^
a.cpp:36:12: 错误:模板第 2 个参数无效
嗯?完全让人摸不着头脑的错误信息。用 Clang 试一试:
Clang Output
❯ clang++ a.cpp -std=c++2a
a.cpp:11:57: error: failed requirement 'std::is_base_of<Base, Other>::value'; 'enable_if' cannot be used to disable this declaration
template<typename T, typename = typename std::enable_if<std::is_base_of<Base, T>::value>::type>
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a.cpp:36:5: note: in instantiation of default argument for 'A<Other>' required here
A<Other> d1;
^~~~~~~~
1 error generated.
哦,这下看出来了,是 std::is_base_of<Base, Other>::value
不满足。
C++ 14
GCC Output
❯ g++ -c a.cpp -std=c++2a
In file included from a.cpp:1:
/usr/include/c++/10.2.0/type_traits: In substitution of ‘template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = void]’:
a.cpp:36:12: required from here
/usr/include/c++/10.2.0/type_traits:2554:11: 错误:no type named ‘type’ in ‘struct std::enable_if<false, void>’
2554 | using enable_if_t = typename enable_if<_Cond, _Tp>::type;
| ^~~~~~~~~~~
a.cpp: 在函数‘int main()’中:
a.cpp:36:12: 错误:模板第 2 个参数无效
36 | B<Other> d1;
| ^
std::is_base_of
还是被 GCC 吞了。同时 Clang 的错误信息也变糟糕了:
Clang Output
❯ clang++ a.cpp -std=c++2a
In file included from a.cpp:1:
/bin/../lib64/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../include/c++/10.2.0/type_traits:2554:44: error: no type named 'type' in 'std::enable_if<false, void>'; 'enable_if' cannot be used to disable this declaration
using enable_if_t = typename enable_if<_Cond, _Tp>::type;
^~~~~
a.cpp:15:38: note: in instantiation of template type alias 'enable_if_t' requested here
template<typename T, typename = std::enable_if_t<std::is_base_of<Base, T>::value>>
^
a.cpp:36:5: note: in instantiation of default argument for 'B<Other>' required here
B<Other> d1;
^~~~~~~~
1 error generated.
C++ 17
错误信息相较 C++ 14 的写法没有任何改进。换句话说,如果你在用 C++ 14 或 C++ 17 的写法,你现在能取得的最好的结果是,编译器告诉你,有一个
std::is_base_of_v<Base, T>
无法满足约束。至于这个 T
是什么,自己猜去吧。
C++ 20
错误信息:
GCC Output
❯ g++ -c a.cpp -std=c++2a
a.cpp: 在函数‘int main()’中:
a.cpp:36:12: 错误:template constraint failure for ‘template<class T> requires is_base_of_v<Base, T> struct D’
36 | D<Other> d1;
| ^
a.cpp:36:12: 附注:constraints not satisfied
a.cpp:24:8: required by the constraints of ‘template<class T> requires is_base_of_v<Base, T> struct D’
a.cpp:23:36: 附注:the expression ‘is_base_of_v<Base, T> [with T = Other]’ evaluated to ‘false’
23 | template<typename T> requires std::is_base_of_v<Base, T>
| ~~~~~^~~~~~~~~~~~~~~~~~~~~
这次我们终于知道 T = Other
了。Clang 的错误信息更清楚一些:
Clang Output
❯ clang++ a.cpp -std=c++2a
a.cpp:36:5: error: constraints not satisfied for class template 'D' [with T = Other]
D<Other> d1;
^~~~~~~~
a.cpp:23:31: note: because 'std::is_base_of_v<Base, Other>' evaluated to false
template<typename T> requires std::is_base_of_v<Base, T>
^
1 error generated.
GCC 从 6 开始支持一个叫做 Concept Lite 的 Concept 实验性版本,MSVC 从 VS2019 16.3 开始支持部分 Concept 的功能。
C++ 每三年才解决一点点问题 由 Peng Guanwen 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。