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

时间:2016-12-02 14:53:16

标签: c++ c++11 c++14 move-semantics perfect-forwarding

考虑以下课程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

由于需要转发到foo1的构造函数,foo2的构造函数是否始终表示初始化成员变量foo1<T>的正确方法?即使用std::forward

更新

以下示例使用foo1 {/ 1}} std::move失败。

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既是引用类型又是值类型。

2 个答案:

答案 0 :(得分:10)

这些示例都没有使用通用引用(转发引用,因为它们现在被称为)。

转发引用仅在存在类型推导时形成,但T&&foo1的构造函数中的foo2不会被推导出来,因此它只是一个右值引用。

由于两者都是右值引用,因此您应在两者上使用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) }
    {
    }
};

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

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) }
    {
    }
};

完美的转发版本:

  • 通过左值 - &gt;一份副本
  • 传递右值 - &gt;一招

对于传递值和移动版本:

  • 通过左值 - &gt;一份,一招
  • 传递右值 - &gt;两个动作

考虑这段代码需要具备的性能,以及需要更改和维护多少代码,并根据它选择一个选项。

答案 1 :(得分:0)

这取决于您推断T的方式。例如:

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

在这种情况下,T中的foo1<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;
};

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