几种类型属性和受支持操作
std::is_copy_constructible / std::is_trivially_copy_constructible / std::is_trivially_copyable
这几种比较看起来容易混淆的概念其实非常好理解。从字面意义上的好理解。
std::is_trivially_copyable
用来检查某个型别是否是可平凡复制的。可平凡复制简而言之就是可以用memcpy这种按字节进行底层逐位拷贝而没有问题的。平凡的意思可以直接去cppreference看。核心来说,平凡的XX函数的含义基本都是这个函数不是由用户提供的。
std::is_trivially_copy_constructible
用来检查某个型别是否有一个平凡的拷贝构造。所以说只要满足is_trivially_copyable,就一定满足is_trivially_copy_constructible
std::is_copy_constructible
检查某个型别是否有个可访问的拷贝构造。
总结
所以说这三种从上到下,是拷贝类型限制的从严格到宽松。
std::is_destructible / std::is_trivially_destructible / std::is_nothrow_destructible
std::is_trivially_destructible
有平凡的析构函数
std::is_nothrow_destructible
析构函数不抛出异常
std::is_destructible
有可访问的析构函数
总结
这一套组更加清晰直观。我们可能只需要知道怎么用一下。一般来说,如果对象是可平凡析构的。那么压根不用调用析构函数。因为析构函数啥也不干。所以在某个实现了std::optional的代码中我们可以看到这种代码段
template <class T, bool = ::std::is_trivially_destructible<T>::value>struct optional_storage_base {//....其他代码 ~optional_storage_base() { if (m_has_value) { m_value.~T(); //显式调用伪析构函数。学名就叫伪析构函数。 m_has_value = false; } }//....其他代码};// 可平凡析构的特化。template <class T>struct optional_storage_base<T, true> {//....其他代码// No destructor, so this class is trivially destructible//....其他代码};延伸
来自PVS Studio的这篇文章的Program execution: trivial types and ABI章节
- trivially_constructible 意味着不需要初始化某些东西。
- trivially_destructible 意味着不需要生成析构函数代码。
- trivially_copyable 意味着除了按字节复制以外不需要做任何其他事情。
- trivially_movable 表达的含义和trivially_copyable 一样,只不过是在移动语境下。
检查是否有用户定义的swap
struct tag {};
template <class T>tag swap(T &, T &);template <class T, std::size_t N>tag swap(T (&a)[N], T (&b)[N]);
template <class, class>std::false_type uses_std(...);template <class T, class U>std::is_same<decltype(swap(std::declval<T &>(), std::declval<U &>())), tag> uses_std(int);
struct myclass { int x;};
void swap(myclass &, myclass &) { std::cout << "1" << std::boolalpha; }int main() { int a = 1; int b = 2; std::cout << std::boolalpha; std::cout << decltype(swap_adl_tests::uses_std<int, int>(0))::value << std::endl; std::cout << decltype(swap_adl_tests::uses_std<myclass, myclass>(0))::value << std::endl; swap(a, b); return 0;}有一个模板合成的swap函数。如果有用户定义的swap函数,那么模板合成的优先级低于用户定义的。用户定义的swap不可能返回tag标签类型。所以如果返回的不是tag类型,标志着不能用std::swap。之所以能找到是因为用了ADL。
相反。如果没有用户定义的swap,我们既没有using namespace std, 也没有using std::swap, 我们也是用无限定名字的swap调用。所以说他看不到std::swap,则肯定只能找到合成的返回tag类型的
获取可变参数包中的最后一个元素
简单粗暴
auto last2 = [](auto... args) { return (args, ...);};来自这里
检查是否是某个类型的特化
template <typename T, template <typename...> typename Template>struct is_specialization : std::false_type {};
template <template <typename...> typename Template, typename... Args>struct is_specialization<Template<Args...>, Template> : std::true_type { void info() { cout << __PRETTY_FUNCTION__ << endl; }};
int main() { static_assert(is_specialization<std::vector<int>, std::vector>{}, ""); is_specialization<std::vector<int>, std::vector>{}.info(); return 0;}
/*void is_specialization<Template<Args ...>, Template>::info() [with Template = std::vector; Args = {int, std::allocator<int>}]*/模板模板参数template就是vector,vector里面的参数就是Args
SFINAE、enable_if 和 enable_if_t 带来的陷阱
本篇文章来自这里的Broken syntax and standard library: std::enable_if_t vs. std::void_t一节
先来看一段代码
struct X { struct Inner {};};
struct Y { struct Outer {};};template <class T>decltype(void(std::declval<typename T::Inner>())) fun(T) { // 1 std::cout << "f1\n";}
template <class T>decltype(void(std::declval<typename T::Outer>())) fun(T) { // 2 std::cout << "f2\n";}int main(){ X x; Y y; fun(x); // 应该输出 "fun1" fun(y); // 应该输出 "fun2"}在“上古”年代,我们会通过这种质朴而纯真的方法来触发SFINAE。一切看起来还可以。直到后面我们有了std::enable_if,陷阱出现了
template <class T>std::enable_if_t<std::is_same_v<typename T::Inner, typename T::Inner>> fun1(T) { // 1 std::cout << "f1\n";}
template <class T>std::enable_if_t<std::is_same_v<typename T::Outer, typename T::Outer>> fun1(T) { // 2 std::cout << "f2\n";}template <class T>std::enable_if<std::is_same_v<typename T::Inner, typename T::Inner>> fun2(T) { // 3 std::cout << "f1\n";}
template <class T>std::enable_if<std::is_same_v<typename T::Outer, typename T::Outer>> fun2(T) { // 4 std::cout << "f2\n";}int main(){ fun1(x); // 应该输出 "f1" fun1(y); // 应该输出 "f2" fun2(x); // 应该输出 "f1" fun2(y); // 应该输出 "f2"}注意,1和2我们使用的是std::enable_if_t, 3和4使用的是std::enable_if
看起来没啥问题对吧。但是它存在一个潜在的问题。
如果我们尝试检查fun1和fun2函数的签名。我们会发现,fun1的函数签名是我们希望的,返回类型是void。但是,fun2的函数签名,返回类型是std::enable_if<true, void>
所以说我们发现了陷阱所在。一个_t后缀可能会导致我们需要花大量时间排查。这同样适用于<type_traits>标头中的所有其他神秘生物。每个std::trait_X和std::trait_X_t在混合使用的时候会导致意图不可见。
编译期表达式计算的例子
来自这里
核心是我们想要优化掉Vec x = a + b + c 这样带有中间结果的情况
template <typename E>class VecExpression { public: static constexpr bool is_leaf = false;
double operator[](size_t i) const { // Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. // virtual functions in C++) return static_cast<E const&>(*this)[i]; } size_t size() const { return static_cast<E const&>(*this).size(); }};
class Vec : public VecExpression<Vec> { std::array<double, 3> elems;
public: static constexpr bool is_leaf = true;
decltype(auto) operator[](size_t i) const { return elems[i]; } decltype(auto) operator[](size_t i) { return elems[i]; } size_t size() const { return elems.size(); }
// construct Vec using initializer list Vec(std::initializer_list<double> init) { std::copy(init.begin(), init.end(), elems.begin()); }
// A Vec can be constructed from any VecExpression, forcing its evaluation. template <typename E> Vec(VecExpression<E> const& expr) { for (size_t i = 0; i != expr.size(); ++i) { elems[i] = expr[i]; } }};template <typename E1, typename E2>class VecSum : public VecExpression<VecSum<E1, E2> > { // cref if leaf, copy otherwise typename std::conditional<E1::is_leaf, const E1&, const E1>::type _u; typename std::conditional<E2::is_leaf, const E2&, const E2>::type _v;
public: static constexpr bool is_leaf = false;
VecSum(E1 const& u, E2 const& v) : _u(u), _v(v) { assert(u.size() == v.size()); } decltype(auto) operator[](size_t i) const { return _u[i] + _v[i]; } size_t size() const { return _v.size(); }};
template <typename E1, typename E2>VecSum<E1, E2> operator+(VecExpression<E1> const& u, VecExpression<E2> const& v) { return VecSum<E1, E2>(*static_cast<const E1*>(&u), *static_cast<const E2*>(&v));}int main(){ Vec v1 = {1, 2, 3}; Vec v2 = {4, 5, 6}; Vec v3 = v1 + v2; std::cout << v3[0] << " " << v3[1] << " " << v3[2] << std::endl;}这样的话,这样 a+b+c 的类型是 VecSum<VecSum<Vec, Vec>, Vec>
Vec x = a + b + c 会调用Vec(VecExpression<E> const& expr)
elems[i] = expr[i];会展开成elems[i] = a.elems[i] + b.elems[i] + c.elems[i]
这样就没有临时Vec对象了
通过值传递还是引用传递可调用对象
来自这里
假设我们有一个函数模板g接受一个可调用对象f,我们用值传递还是引用传递?
template <class F>void g(F f); // (1) by value
template <class F>void g(F&& f); // (2) by reference如果我们看看标准库,比如 std::invoke, 可调用对象是引用传递
template< class F, class... Args >std::invoke_result_t<F, Args...> invoke( F&& f, Args&&... args ) noexcept(/* see below */);在其他大多数情况,比如在 <algorithm>头文件中,可调用对象是值传递
template< class T, class Compare >const T& max( const T& a, const T& b, Compare comp );所以按照作者意见:
- 如果
g的主要作用是调用f,则使用引用 - 其他情况下使用值传递。
这样做的好处是可以简化一些代码,同时可以让可调用对象直接被algotirhm使用
对于那些不可拷贝,或者不想拷贝的可调用对象。可以使用std::ref包装
运算符重载
从最近需求中学习,也感谢磊哥的指点。我们有现在的SSCCE代码
struct t1 { int a; int b;};
template <typename T1>struct F { F(int x) : val_(x) {}; F() : val_(2) {}; template <typename T2> friend F<T1> operator*(const F<T1>& a, const F<T2>& b) { return a.val_ * b.val_; } private: int val_;};int main() { F<int> a; F<t1> b(3); auto s = a * b; // NOPE cout << s.getval() << endl;}/*1107.cpp:16:27: error: ‘int F<t1>::val_’ is private within this context 16 | return a.val_ * b.val_; | ~~^~~~1107.cpp:24:9: note: declared private here 24 | int val_; | ^~~~*/我们有一个乘法运算符重载。我们不希望写为成员函数,希望是友元函数。同时我们不想暴露某些私有方法或成员。但是现在我们似乎发现F<T1>看不到F<T2>的成员。这是必然的。因为类模板F<X>和F<Y>并不是同一类型。同时,这种友元函数的定义实际上并不是一个全局模板函数,而是一个成员模板函数。在这种情况下,operator* 只有在 F<T1> 的实例内部才是友元。对于 F<T2> 类型的对象,F<T1> 的 operator* 无法访问 F<T2> 的私有成员 val_。因此编译器会报错。
所以我们用如下方法:
struct t1 { int a; int b;};
template <typename T1>struct F { F(int x) : val_(x) {}; F() : val_(2) {}; template <typename T11, typename T2> // T11不可是T1,会shadow friend F<T11> operator*(const F<T11>& a, const F<T2>& b); int getval(){ return val_; } private: int val_;};template<typename T11, typename T2> //T11可以是任何,可以是T1, 无所谓F<T11> operator* (const F<T11>& a, const F<T2>& b){ return a.val_ * b.val_;}int main() { F<int> a; F<t1> b(3); auto s = a * b; // GOOD cout << s.getval() << endl;}这种写法,这里声明了一个独立的全局模板函数,并且声明了它为友元函数。由于这是一个独立的模板函数,它可以访问 F<T11> 和 fuckFT2> 的私有成员。这是因为编译器在第二种情况下将operator*视为所有 F<T1> 和 F<T2> 的友元函数,因此无论 F<T11> 和 F<T2> 是什么类型,它们的私有成员 val_ 都可以被访问。
可以查看这里的 模板类的非约束模板友元函数