当前位置: 首页 > 工具软件 > Enable Copy > 使用案例 >

移动语义(Move Semantics)与 enable_if<>

松国兴
2023-12-01

移动语义(Move Semantics)无疑是 C++11 最重要的特性,可以用于优化拷贝和赋值操作。本章将讨论下移动语义对模板设计的影响。移动语义和完美转发相关文章也贴在这里,供参考:

Item 23: Understand std::move and std::forward.
Item 24: Distinguish universal references from rvalue references.
Item 25: Use std::move on rvalue references, std::forward on universal references.
Item 26: Avoid overloading on universal references.
Item 27: Familiarize yourself with alternatives to overloading on universal references.
Item 28: Understand reference collapsing.
Item 29: Assume that move operations are not present, not cheap, and not used.
Item 30: Familiarize yourself with perfect forwarding failure cases.

完美转发

将一个函数实参的如下基本特性转发给另一个函数:

  • 可修改的对象转发后应该仍可以被修改。
  • 常量对象应该作为只读对象转发。
  • 可移动的对象应该作为可移动对象转发。

如果不使用模板,你需要重载上面的所有情况,假设转发 f() 的参数给 g()

#include <utility>
#include <iostream>

class X {
  ...
};

void g (X&) {
  std::cout << "g() for variable\n";
}

void g (X const&) {
  std::cout << "g() for constant\n";
}

void g (X&&) {
  std::cout << "g() for movable object\n";
}

// let f() forward argument val to g():
void f (X& val) {
  g(val);               // val is non-const lvalue => calls g(X&)
}

void f (X const& val) {
  g(val);              // val is const lvalue => calls g(X const&)
}

void f (X&& val) {
  g(std::move(val));   // val is non-const lvalue => needs std::move() to call g(X&&)
}

int main()
{
  X v;              // create variable
  X const c;        // create constant
  f(v);             // f() for nonconstant object calls f(X&) => calls g(X&)
  f(c);             // f() for constant object calls f(X const&) => calls g(X const&)
  f(X());           // f() for temporary calls f(X&&) => calls g(X&&)
  f(std::move(v));  // f() for movable variable calls f(X&&) => calls g(X&&)
}

需要注意的是第三个 f(),虽然 val 被申明为一个右值引用,但它本身依然是一个非常量左值,如果不使用 std::move 进行转换,将会调用第一个 f()

void f (X&& val) {
  g(std::move(val));   // val is non-const lvalue => needs std::move() to call g(X&&)
}

C++11 引入的万能引用(Universal Reference)和完美转发(Perfect Forward)机制可以大大简化代码:

template<typename T>
void f (T&& val) {
  g(std::forward<T>(val));  // perfect forward val to g()
}

使用万能引用和完美转发版本的 f() 则可以完美替代上述三个版本的 f()。此处需要注意:对于模板参数 TT&& 和一个特定类型 XX&& 则完全不同。

  • 对于特定类型 XX&& 是一个右值引用,它只能绑定到一个可移动的对象(a prvalue, such as a temporary object, and an xvalue, such as an object passed with std::move())上。
  • 对于模板参数 TT&& 是一个万能引用(universal reference,or called perfect reference),它可以绑定到一个可变的、不可变的、可移动的对象上。

T 必须是模板参数,模板的依赖参数不是万能引用。例如 T::iterator&& 只是一个右值引用,而非万能引用。

借助可变参数模板,完美转发也可以转发任意数量参数:

template<typename... Ts>
void f(Ts&&... args)
{
  g(std::forward<Ts>(args)...);
}

特殊成员函数模板

成员函数模板也可以用于特殊的成员函数,例如构造函数,但可能产生一些奇怪的行为。

考虑下面代码:

#include <utility>
#include <string>
#include <iostream>
class Person
{
  private:
    std::string name;
  public:
    // constructor for passed initial name:
    explicit Person(std::string const& n) : name(n) {
      std::cout << "copying string-CONSTR for '" << name << "'\n";
    }
    explicit Person(std::string&& n) : name(std::move(n)) {
      std::cout << "moving string-CONSTR for '" << name << "'\n";
    }
    // copy and move constructor:
    Person (Person const& p) : name(p.name) {
      std::cout << "COPY-CONSTR Person '" << name << "'\n";
    }
    Person (Person&& p) : name(std::move(p.name)) {
      std::cout << "MOVE-CONSTR Person '" << name << "'\n";
    }
};

int main()
{
  std::string s = "sname";
  Person p1(s);              // init with string object => calls copying string-CONSTR
  Person p2("tmp");          // init with string literal => calls moving string-CONSTR
  Person p3(p1);             // copy Person => calls COPY-CONSTR
  Person p4(std::move(p1));  // move Person => calls MOVE-CONST
}

如果我们使用万能引用和完美转发机制重写前两个 std::string 的构造函数:

#include <iostream>
class Person
{
  private:
    std::string name;
  public:
    // generic constructor for passed initial name:
    template<typename STR>
    explicit Person(STR&& n) : name(std::forward<STR>(n)) {
      std::cout << "TMPL-CONSTR for '" << name << "'\n";
    }
    // copy and move constructor:
    Person (Person const& p) : name(p.name) {
      std::cout << "COPY-CONSTR Person '" << name << "'\n";
    }
    Person (Person&& p) : name(std::move(p.name)) {
      std::cout << "MOVE-CONSTR Person '" << name << "'\n";
    }
};

int main()
{
  std::string s = "sname";
  Person p1(s);              // init with string object => calls TMPL-CONSTR
  Person p2("tmp");          // init with string object => calls TMPL-CONSTR
  Person p3(p1);             // ERROR
  Person p4(std::move(p1));  // OK: move Person => calls MOVE-CONST
  Person const p2c("ctmp");  // init constant object with string literal
  Person p3c(p2c);           // OK: copy constant Person => calls COPY-CONSTR
}

p1p2p4 的构造函数调用符合我们的预期,但是 p3 的构造将失败。这是因为完美转发构造函数的模板比拷贝构造函数更匹配 non-const 左值,于是调用完美转换构造函数,导致使用 Person 类型初始化 std::string 的错误。

Person p3(p1); 

而拷贝一个 const 的类型的 Person,则不会有问题。这是因为当模板函数和普通函数都能完美匹配时,则优先调用普通函数。

Person const p2c("ctmp");  // init constant object with string literal
Person p3c(p2c);           // OK: copy constant Person => calls COPY-CONSTR

对于上面问题的一个解决方案是补充一个非 const 参数的拷贝构造函数:

Person(Person&);

但这不是一个完美的解决方案,对于继承类,完美转发的成员模板构造函数将更加匹配。可以参考文章开头的参考文章 Item 26: Avoid overloading on universal references. 的相关介绍。完美的解决方案是使用 enable_if<> 来禁用某些参数类型的实例化。

使用 enable_if<> 禁用模板

C++11 引入 enable_if<>,使模板根据编译期条件进行实例化。例如:

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

如果表达式 (sizeof(T) > 4false,该定义将被忽略,即模板不会被实例化。否则,才会实例化。

std::enable_if 是一个 type traits,编译时表达式作为首个模板实参传递。若表达式为 false,则 enable_if::type 未定义,由于被称为 SFINAE(substitution failure is not an error)的模板特性,这个带有 enable_if 的函数模板将被忽略。若表达式为 trueenable_if::type 产生一个类型,若有第二个实参,则类型为第二个实参类型,否则为 void

template<typename T>
std::enable_if<(sizeof(T) > 4), T>::type 
foo() {
  return T();
}

C++14 为所有的 type traits 提供了别名模板。例如 enable_if::type 可以简写为 enable_if_t,则上述代码可以简化为:

template<typename T>
typename std::enable_if_t<(sizeof(T) > 4)>
foo() {
}

template<typename T>
typename std::enable_if_t<(sizeof(T) > 4), T>
foo() {
  return T();
}

为了增加可读性,可以将 enable_if 作为第二个模板参数:

template<typename T,
         typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}

如果 (sizeof(T) > 4true,则可扩展为:

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

还可以使用 using 进一步简化:

template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;

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

使用 enable_if<>

对于前面的问题,我们期望的是 std::string 或者可以转为为 std::string 类型的参数才匹配到完美转发构造函数。

template<typename STR>
Person(STR&& n);

借助 enable_if<>std::is_convertible,并利用 C++17 的写法:std::is_convertible_v 替代 std::is_convertible::value ,实现如下:

template<typename STR,
         typename = std::enable_if_t<
                      std::is_convertible_v<STR, std::string>>>
Person(STR&& n);

如果 STR 是可以转换为 std::string 类型,则上述声明可以扩展为:

template<typename STR,
         typename = void>
Person(STR&& n);

同样地,可以借助 using 简化为:

template<typename T>
using EnableIfString = std::enable_if_t<
                         std::is_convertible_v<T, std::string>>;
...
template<typename STR, typename = EnableIfString<STR>>
Person(STR&& n);

这里给出完整代码:

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>
template<typename T>
using EnableIfString = std::enable_if_t<
                         std::is_convertible_v<T,std::string>>;
class Person
{
  private:
    std::string name;
  public:
    // generic constructor for passed initial name:
    template<typename STR, typename = EnableIfString<STR>>
    explicit Person(STR&& n)
     : name(std::forward<STR>(n)) {
      std::cout << "TMPL-CONSTR for '" << name << "'\n";
    }
    // copy and move constructor:
    Person (Person const& p) : name(p.name) {
      std::cout << "COPY-CONSTR Person '" << name << "'\n";
    }
    Person (Person&& p) : name(std::move(p.name)) {
      std::cout << "MOVE-CONSTR Person '" << name << "'\n";
    }
};

int main()
{
  std::string s = "sname";
  Person p1(s);              // init with string object => calls TMPL-CONSTR
  Person p2("tmp");          // init with string literal => calls TMPL-CONSTR
  Person p3(p1);             // OK => calls COPY-CONSTR
  Person p4(std::move(p1));  // OK => calls MOVE-CONST
}

也可以用 std::is_constructible 替代 std::is_convertible,但 std::is_convertible 判断的是类型可以隐式转换,而 std::is_constructible 判断的是显式转换,实参顺序相反。

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 (T const&) {
      std::cout << "tmpl copy constructor\n";
    }
    ...
};

C x;
C y{x}; // still uses the predefined copy constructor (not the member template)

一个解决办法是使用 =delete 标记预定义的特殊成员函数。

class C
{
  public:
    ...
    // user-define the predefined copy constructor as deleted
    // (with conversion to volatile to enable better matches)
    C(C const volatile&) = delete;
    // implement copy constructor template with better match:
    template<typename T>
    C(T const&) {
      std::cout << "tmpl copy constructor\n";
    }
    ...
};

C x;
C y{x}; // uses the member template

此时就可以用 enable_if<> 添加编译期限制,比如模板参数类型为整型时禁用拷贝构造:

template<typename T>
class C
{
  public:
    ...
    // user-define the predefined copy constructor as deleted
    // (with conversion to volatile to enable better matches)
    C(C const volatile&) = delete;
    // if T is no integral type, provide copy constructor template with better match:
    template<typename U,
             typename = std::enable_if_t<!std::is_integral<U>::value>>
    C (C<U> const&) {
      ...
    }
    ...
};

使用 Concepts 简化 enable_if<> 表达式

使用 enable_if<> 使用起来比较繁琐,并且可读性比较差。C++20 引入 Concepts 可以大大简化 enable_if<> 表达式。例如:

template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n) : name(std::forward<STR>(n)) {
  ...
}

也可以简化为:

template<typename T>
concept ConvertibleToString = std::is_convertible_v<T,std::string>;

template<typename STR>
requires ConvertibleToString<STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}

甚至可以简化为:

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

至此,本文结束。

参考:

  • http://www.tmplbook.com
 类似资料: