std::move 或 std::forward 在 C++ 中将通用构造函数分配给成员变量时

std::move or std::forward when assigning universal constructor to member variable in C++

考虑以下 类 foo1foo2

template <typename T>
struct foo1
{
    T t_;

    foo1(T&& t) :
        t_{ std::move(t) }
    {
    }
};

template <typename T>
struct foo2
{
    foo1<T> t_;

    foo2(T&& t) :
        t_{ std::forward<T>(t) }
    {
    }
};

总是foo1的构造函数代表初始化成员变量T的正确方式吗?即通过使用 std::move.

foo2 的构造函数是否总是代表初始化成员变量 foo1<T> 的正确方法,因为需要转发到 foo1 的构造函数?即通过使用 std::forward.

更新

以下示例使用 std::movefoo1 失败:

template <typename T>
foo1<T> make_foo1(T&& t)
{
    return{ std::forward<T>(t) };
}

struct bah {};

int main()
{
    bah b;

    make_foo1(b); // compiler error as std::move cannot be used on reference

    return EXIT_SUCCESS;
}

这是个问题,因为我希望 T 既是引用类型又是值类型。

这些示例都没有使用通用引用(转发引用,现在这样称呼)。

转发引用仅在存在类型推导的情况下形成,但 foo1foo2 的构造函数中的 T&& 未被推导,因此它只是一个右值引用。

因为两者都是右值引用,所以你应该对两者都使用 std::move

如果你想使用转发引用,你应该让构造函数有一个推导的模板参数:

template <typename T>
struct foo1
{
    T t_;

    template <typename U>
    foo1(U&& u) :
        t_{ std::forward<U>(u) }
    {
    }
};

template <typename T>
struct foo2
{
    foo1<T> t_;

    template <typename U>
    foo2(U&& u) :
        t_{ std::forward<U>(u) }
    {
    }
};

在这种情况下,您不应在 foo1 中使用 std::move,因为客户端代码可以传递一个左值并让对象静默失效:

std::vector<int> v {0,1,2};
foo1<std::vector<int>> foo = v;
std::cout << v[2]; //yay, undefined behaviour

一种更简单的方法是按值无条件地 std::move 进入存储:

template <typename T>
struct foo1
{
    T t_;

    foo1(T t) :
        t_{ std::move(t) }
    {
    }
};

template <typename T>
struct foo2
{
    foo1<T> t_;

    foo2(T t) :
        t_{ std::move(t) }
    {
    }
};

求完美转发版本:

  • 传递左值 -> 一份
  • 传递右值 -> 一步

对于按值传递和移动版本:

  • 左值传递 -> 一份,一步
  • 传递右值 -> 两步

考虑此代码的性能需要如何以及需要更改和维护的程度,然后根据此选择一个选项。

这取决于你如何推断​​T。例如:

template<class T>
foo1<T> make_foo1( T&& t ) {
  return std::forward<T>(t);
}

在这种情况下,foo1<T> 中的 T 是转发引用,您的代码将无法编译。

std::vector<int> bob{1,2,3};
auto foo = make_foo1(bob);

上面的代码在构造函数中从 bob 默默地移动到 std::vector<int>&foo1<std::vector<int>&>.

foo2 做同样的事情会奏效。你会得到一个 foo2<std::vector<int>&>,它会包含对 bob.

的引用

当你写模板的时候,你必须考虑T类型被引用意味着什么。如果您的代码不支持它作为参考,请考虑 static_assert 或 SFINAE 阻止这种情况。

template <typename T>
struct foo1 {
  static_assert(!std::is_reference<T>{});
  T t_;

  foo1(T&& t) :
    t_{ std::move(t) }
  {
  }
};

现在这段代码生成一条合理的错误消息。

您可能认为现有的错误消息没问题,但这只是因为我们进入了 T.

template <typename T>
struct foo1 {
  static_assert(!std::is_reference<T>{});

  foo1(T&& t)
  {
    auto internal_t = std::move(t);
  }
};

这里只有 static_assert 确保我们的 T&& 是实际的右值。


但是这个理论上的问题列表已经足够了。你有一个具体的。

最后这大概就是你想要的:

template <class T> // typename is too many letters
struct foo1 {
  static_assert(!std::is_reference<T>{});
  T t_;

  template<class U,
    class dU=std::decay_t<U>, // or remove ref and cv
    // SFINAE guard required for all reasonable 1-argument forwarding
    // reference constructors:
    std::enable_if_t<
      !std::is_same<dU, foo1>{} && // does not apply to `foo1` itself
      std::is_convertible<U, T> // fail early, instead of in body
    ,int> = 0
  >
  foo1(U&& u):
    t_(std::forward<U>(u))
  {}
  // explicitly default special member functions:
  foo1()=default;
  foo1(foo1 const&)=default;
  foo1(foo1 &&)=default;
  foo1& operator=(foo1 const&)=default;
  foo1& operator=(foo1 &&)=default;
};

或者,在 99/100 情况下同样有效的更简单的情况:

template <class T>
struct foo1 {
  static_assert(!std::is_reference<T>{});
  T t_;

  foo1(T t) :
    t_{ std::move(t) }
  {}
  // default special member functions, just because I never ever
  // want to have to memorize the rules that makes them not exist
  // or exist based on what other code I have written:
  foo1()=default;
  foo1(foo1 const&)=default;
  foo1(foo1 &&)=default;
  foo1& operator=(foo1 const&)=default;
  foo1& operator=(foo1 &&)=default;
};

作为一般规则,这种更简单的技术比完美的转发技术多了 1 个移动,以换取大量更少的代码和复杂性。它允许 {} 初始化构造函数的 T t 参数,这很好。