完美转发

  • 要用一个函数将实参的如下基本属性转发给另一个函数
    • 可修改的对象转发后应该仍可以被修改
    • 常量对象应该作为只读对象转发
    • 可移动的对象应该作为可移动对象转发
  • 如果不使用模板实现这些功能,必须编写全部的三种情况 ```cpp void f(int&) { std::cout << 1; } void f(const int&) { std::cout << 2; } void f(int&&) { std::cout << 3; }

// 用多个重载转发给对应版本比较繁琐 void g(int& x) { f(x); }

void g(const int& x) { f(x); }

void g(int&& x) { f(std::move(x)); }

// 同样可以用一个模板来替代上述功能 template void h(T&& x) { f(std::forward(x)); // 注意std::forward的模板参数是T }

int main() { int a = 1; const int b = 1;

g(a); h(a); // 11 g(b); h(b); // 22 g(std::move(a)); h(std::move(a)); // 33 g(1); h(1); // 33 }

  1. - 结合可变参数模板,完美转发可以转发任意数量的实参
  2. ```cpp
  3. template<typename... Ts>
  4. void f(Ts&&... args)
  5. {
  6. g(std::forward<Ts>(args)...); // 把任意数量的实参转发给g
  7. }
  • lambda中也可以使用完美转发 ```cpp auto f = { return g(std::forward(x)); };

// 转发任意数量实参 auto f = { return g(std::forward(args)…); };

  1. - 如果想在转发前修改要转发的值,可以用auto&&存储结果,修改后再转发
  2. ```cpp
  3. template<typename T>
  4. void f(T x)
  5. {
  6. auto&& res = doSomething(x);
  7. doSomethingElse(res);
  8. set(std::forward<decltype(res)>(res));
  9. }

特殊成员函数模板

  • 模板也能用于特殊的成员函数,如构造函数,但这可能导致意外的行为
  • 下面是未使用模板的代码 ```cpp class Person { public: explicit Person(const std::string& n) : name(n) {} // 拷贝初始化函数 explicit Person(std::string&& n) : name(std::move(n)) {} // 移动初始化函数 Person(const Person& p) : name(p.name) {} // 拷贝构造函数 Person(Person&& p) : name(std::move(p.name)) {} // 移动构造函数 private: std::string name; };

int main() { std::string s = “sname”; Person p1(s); // 调用拷贝初始化函数 Person p2(“tmp”); // 调用移动初始化函数 Person p3(p1); // 调用拷贝构造函数 Person p4(std::move(p1)); // 调用移动构造函数 }

  1. - 现在用模板作为构造函数,完美转发实参给成员name,替代原有的两个构造函数
  2. ```cpp
  3. class Person {
  4. public:
  5. template<typename STR> // 完美转发构造函数
  6. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  7. Person(const Person& p) : name(p.name) {} // 拷贝构造函数
  8. Person(Person&& p) : name(std::move(p.name)) {} // 移动构造函数
  9. private:
  10. std::string name;
  11. };
  • 如下构造函数仍能正常工作

    1. std::string s = "sname";
    2. Person p1(s); // 调用完美转发构造函数
    3. Person p2("tmp"); // 调用完美转发构造函数
    4. Person p4(std::move(p1)); // 调用移动构造函数
  • 但调用拷贝构造函数时将出错

    1. Person p3(p1); // 错误
  • 但拷贝一个const对象却不会出错

    1. const Person p2c("ctmp"); // 调用完美转发构造函数
    2. Person p3c(p2c); // 调用拷贝构造函数
  • 原因是,成员模板比拷贝构造函数更匹配non-const左值,于是调用完美转换构造函数,导致使用Person类型初始化std::string的错误

    1. // 对于Person p1的匹配
    2. template<typename STR>
    3. Person(STR&&)
    4. // 优于
    5. Person(const Person&)
  • 一种解决方法是添加一个接受non-const实参的拷贝构造函数

    1. Person(Person&);
  • 但这只是一个局限的解决方案,因为对于派生类对象,成员模板仍然是更好的匹配。最佳方案是在传递实参为Person或一个能转换为Person的表达式时,禁用成员模板

使用enable_if禁用成员模板

  • C++11提供的std::enable_if允许在某个编译期条件下忽略函数模板 ```cpp

    include

template typename std::enable_if<(sizeof(T) > 4)>::type f() {}

  1. - 如果(sizeof(T) > 4)为false,模板的定义将被忽略,若为true则模板实例扩展为
  2. ```cpp
  3. void f() {}
  • std::enable_if是一个type traits,编译期表达式作为首个模板实参传递。若表达式为false,则enable_if::type未定义,由于SFINAE,这个带有enable_if的函数模板将被忽略。若表达式为true,enable_if::type产生一个类型,若有第二个实参,则类型为第二个实参类型,否则为void

    1. template<typename T>
    2. std::enable_if<(sizeof(T) > 4), T>::type f()
    3. {
    4. return T();
    5. }
  • C14中为所有type traits提供了一个别名模板,以省略后缀::type,enable_if::type在C14中可以简写为enable_if_t

    1. template<typename T>
    2. std::enable_if_t<(sizeof(T) > 4)>
    3. f() {}
  • enable_if表达式出现在声明中很影响可读性,因此std::enable_if常用作一个额外的模板实参的默认值

    1. template<typename T, typename = std::enable_if_t<(sizeof(T) > 4)>>
    2. void f() {}
  • 如果sizeof(T) > 4,扩展为

    1. template<typename T, typename = void>
    2. void f() {}
  • 利用别名模板还可以进一步简化代码 ```cpp template using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;

template> void f() {}

  1. <a name="bbcb7b84"></a>
  2. ## enable_if解决完美转发构造函数的优先匹配
  3. - 现在来解决之前的构造函数模板的问题,当实参STR有正确的类型([std::string](https://zh.cppreference.com/w/cpp/string/basic_string)或可以转换为[std::string](https://zh.cppreference.com/w/cpp/string/basic_string)的类型)时禁用完美转发构造函数。为此,需要使用另一个[type traits](https://zh.cppreference.com/w/cpp/header/type_traits),[std::is_convertible](https://zh.cppreference.com/w/cpp/types/is_convertible)
  4. ```cpp
  5. template<typename STR,
  6. typename = std::enable_if_t<std::is_convertible<STR, std::string>::value>>
  7. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 如果STR不能转换为std::string,则模板将被忽略。如果STR可转换为std::string,则整个声明扩展为

    1. template<typename STR, typename = void>
    2. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 同样也可以使用别名模板定义自己的名称,此外C++17中可以用别名模板std::is_convertible_v替代std::is_convertible::value,最终代码如下 ```cpp

    include

    include

    include

template using EnableIfString = std::enable_if_t>;

class Person { public: template> explicit Person(STR&& n) : name(std::forward(n)) {} Person(const Person& p) : name(p.name) {} Person(Person&& p) : name(std::move(p.name)) {} private: std::string name; };

int main() { std::string s = “sname”; Person p1(s); // 调用完美转发构造函数 Person p2(“tmp”); // 调用完美转发构造函数 Person p3(p1); // 调用拷贝构造函数 Person p4(std::move(p1)); // 调用移动构造函数 }

  1. - 也可以用[std::is_constructible](https://zh.cppreference.com/w/cpp/types/is_constructible)替代[std::is_convertible](https://zh.cppreference.com/w/cpp/types/is_convertible),但要注意[std::is_convertible](https://zh.cppreference.com/w/cpp/types/is_convertible)判断类型可以隐式转换,而[std::is_constructible](https://zh.cppreference.com/w/cpp/types/is_constructible)判断的是显式转换,实参顺序相反
  2. ```cpp
  3. template<typename T>
  4. using EnableIfString = std::enable_if_t<std::is_constructible_v<std::string, T>>;

模板与预定义的特殊成员函数

  • 通常不能用enable_if禁用预定义的拷贝/移动构造函数和赋值运算符,因为成员函数模板不会被当作特殊的成员函数,比如当需要拷贝构造函数时,成员模板将被忽略 ```cpp class C { public: template C(const T&) {} };

C x; C y{x}; // 仍然使用预定义合成的拷贝构造函数,上面的模板被忽略

  1. - 同样不能删除预定义的拷贝构造函数,但有一个tricky方案,可以为cv限定符修饰的实参声明一个拷贝构造函数,这样会禁止合成拷贝构造函数。再将自定义的拷贝构造函数声明为=delete,这样模板就会成为唯一选择
  2. ```cpp
  3. class C {
  4. public:
  5. C(const volatile C&) = delete; // 显式声明将阻止默认合成拷贝构造函数
  6. template<typename T>
  7. C(const T&) {}
  8. };
  9. C x;
  10. C y{x}; // 使用模板
  • 此时就可以用enable_if添加限制,比如模板参数类型为整型时禁用拷贝构造
    1. template<typename T>
    2. class C {
    3. public:
    4. C(const volatile C&) = delete;
    5. template<typename U, typename = std::enable_if_t<!std::is_integral_v<U>>>
    6. C(const C<U>&) {}
    7. };

使用concepts替代enable_if

  • concepts已经成为C++20标准的一部分,对于之前的

    1. template<typename STR,
    2. typename = std::enable_if_t<std::is_convertible_v<STR, std::string>>>
    3. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 使用concepts可以写为

    1. template<typename STR>
    2. requires std::is_convertible_v<STR, std::string>
    3. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 同样为了方便,类似于别名模板,可以把requirement制定为一个通用的concept

    1. template<typename T>
    2. concept ConvertibleToString = std::is_convertible_v<T,std::string>;
  • 再将这个concept作为requirement使用

    1. template<typename STR>
    2. requires ConvertibleToString<STR>
    3. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 也可以直接写为

    1. template<ConvertibleToString STR>
    2. explicit Person(STR&& n) : name(std::forward<STR>(n)) {}