06 移动语义与 enable_if

完美转发(Perfect Forwarding)

  • 要用一个函数将实参的如下基本属性转发给另一个函数
    • 可修改的对象转发后应该仍可以被修改
    • 常量对象应该作为只读对象转发
    • 可移动的对象应该作为可移动对象转发
  • 如果不使用模板实现这些功能,必须编写全部的三种情况
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<typename T>
void h(T&& x)
{
  f(std::forward<T>(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
}
  • 结合可变参数模板,完美转发可以转发任意数量的实参
template<typename... Ts>
void f(Ts&&... args)
{
  g(std::forward<Ts>(args)...); // 把任意数量的实参转发给g
}
  • lambda中也可以使用完美转发
auto f = [](auto&& x) { return g(std::forward<decltype(x)>(x)); };

// 转发任意数量实参
auto f = [](auto&&... args) {
  return g(std::forward<decltype(args)>(args)...);
};
  • 如果想在转发前修改要转发的值,可以用auto&&存储结果,修改后再转发
template<typename T>
void f(T x)
{
  auto&& res = doSomething(x);
  doSomethingElse(res);
  set(std::forward<decltype(res)>(res));
}

特殊成员函数模板

  • 模板也能用于特殊的成员函数,如构造函数,但这可能导致意外的行为
  • 下面是未使用模板的代码
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)); // 调用移动构造函数
}
  • 现在用模板作为构造函数,完美转发实参给成员name,替代原有的两个构造函数
class Person {
 public:
  template<typename STR> // 完美转发构造函数
  explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  Person(const Person& p) : name(p.name) {} // 拷贝构造函数
  Person(Person&& p) : name(std::move(p.name)) {} // 移动构造函数
 private:
  std::string name;
};
  • 如下构造函数仍能正常工作
std::string s = "sname";
Person p1(s); // 调用完美转发构造函数
Person p2("tmp"); // 调用完美转发构造函数
Person p4(std::move(p1)); // 调用移动构造函数
  • 但调用拷贝构造函数时将出错
Person p3(p1); // 错误
  • 但拷贝一个const对象却不会出错
const Person p2c("ctmp"); // 调用完美转发构造函数
Person p3c(p2c); // 调用拷贝构造函数
  • 原因是,成员模板比拷贝构造函数更匹配non-const左值,于是调用完美转换构造函数,导致使用Person类型初始化std::string的错误
// 对于Person p1的匹配
template<typename STR>
Person(STR&&)
// 优于
Person(const Person&)
  • 一种解决方法是添加一个接受non-const实参的拷贝构造函数
Person(Person&);
  • 但这只是一个局限的解决方案,因为对于派生类对象,成员模板仍然是更好的匹配。最佳方案是在传递实参为Person或一个能转换为Person的表达式时,禁用成员模板

使用enable_if禁用成员模板

  • C++11提供的std::enable_if允许在某个编译期条件下忽略函数模板
#include <type_traits>

template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
f() {}
  • 如果(sizeof(T) > 4)为false,模板的定义将被忽略,若为true则模板实例扩展为
void f() {}
  • std::enable_if是一个type traits,编译期表达式作为首个模板实参传递。若表达式为false,则enable_if::type未定义,由于SFINAE,这个带有enable_if的函数模板将被忽略。若表达式为true,enable_if::type产生一个类型,若有第二个实参,则类型为第二个实参类型,否则为void
template<typename T>
std::enable_if<(sizeof(T) > 4), T>::type f()
{
  return T();
}
  • C++14中为所有type traits提供了一个别名模板,以省略后缀::type,enable_if::type在C++14中可以简写为enable_if_t
template<typename T>
std::enable_if_t<(sizeof(T) > 4)>
f() {}
  • enable_if表达式出现在声明中很影响可读性,因此std::enable_if常用作一个额外的模板实参的默认值
template<typename T, typename = std::enable_if_t<(sizeof(T) > 4)>>
void f() {}
  • 如果sizeof(T) > 4,扩展为
template<typename T, typename = void>
void f() {}
  • 利用别名模板还可以进一步简化代码
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;

template<typename T, typename = EnableIfSizeGreater4<T>>
void f() {}

使用enable_if解决完美转发构造函数的优先匹配问题

template<typename STR,
  typename = std::enable_if_t<std::is_convertible<STR, std::string>::value>>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 如果STR不能转换为std::string,则模板将被忽略。如果STR可转换为std::string,则整个声明扩展为
template<typename STR, typename = void>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 同样也可以使用别名模板定义自己的名称,此外C++17中可以用别名模板std::is_convertible_v替代std::is_convertible::value,最终代码如下
#include <iostream>
#include <string>
#include <type_traits>

template<typename T>
using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>;

class Person {
 public:
  template<typename STR, typename = EnableIfString<STR>>
  explicit Person(STR&& n) : name(std::forward<STR>(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)); // 调用移动构造函数
}
template<typename T>
using EnableIfString = std::enable_if_t<std::is_constructible_v<std::string, T>>;

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

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

C x;
C y{x}; // 仍然使用预定义合成的拷贝构造函数,上面的模板被忽略
  • 同样不能删除预定义的拷贝构造函数,但有一个tricky方案,可以为cv限定符修饰的实参声明一个拷贝构造函数,这样会禁止合成拷贝构造函数。再将自定义的拷贝构造函数声明为=delete,这样模板就会成为唯一选择
class C {
 public:
  C(const volatile C&) = delete; // 显式声明将阻止默认合成拷贝构造函数
  template<typename T>
  C(const T&) {}
};

C x;
C y{x}; // 使用模板
  • 此时就可以用enable_if添加限制,比如模板参数类型为整型时禁用拷贝构造
template<typename T>
class C {
 public:
  C(const volatile C&) = delete;
  template<typename U, typename = std::enable_if_t<!std::is_integral_v<U>>>
  C(const C<U>&) {}
};

使用concepts替代enable_if以简化表达式

  • concepts经过长时间讨论仍然还未成为C++17标准的一部分,但一些编译器提供了实验性的支持,对于之前的
template<typename STR,
  typename = std::enable_if_t<std::is_convertible_v<STR, std::string>>>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 使用concepts可以写为
template<typename STR>
requires std::is_convertible_v<STR, std::string>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 同样为了方便,类似于别名模板,可以把requirement制定为一个通用的concept
template<typename T>
concept ConvertibleToString = std::is_convertible_v<T,std::string>;
  • 再将这个concept作为requirement使用
template<typename STR>
requires ConvertibleToString<STR>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
  • 也可以直接写为
template<ConvertibleToString STR>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • 按值传递 按值传递实参时,原则上会拷贝每个实参,对于类通常还需要调用拷贝构造函数。调用拷贝构造函数可能开销很大,但...
    奇点创客阅读 231评论 0 0
  • 类模板示例 使用类模板 模板实参可以是任何类型 成员函数只有被调用到时才实例化 如果类模板有static数据成员,...
    奇点创客阅读 130评论 0 0
  • 本文按照 cppreference[https://en.cppreference.com/w/] 列出的特性列表...
    401阅读 20,922评论 2 18
  • 可变参数模板示例 重载可变参数和非可变参数模板 前一例子也可以如下实现,如果两个函数模板只有尾置参数包不同,会优先...
    奇点创客阅读 178评论 0 0
  • 1. C++14 参考文档[https://en.cppreference.com/w/cpp/14] 不同于重量...
    Platanuses阅读 792评论 0 1