STD :: MOVE或STD ::向前转向C 中的成员变量时

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

本文关键字:STD 成员 变量 MOVE      更新时间:2023-10-16

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

update

使用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&&并未推导,因此它只是RVALUE参考。

由于两者都是rvalue参考,因此您应该在两者上使用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,因为客户端代码可以通过lvalue并使对象无效地无效:

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

对于完美的转发版本:

  • 通过lvalue->一个副本
  • 通过了rvalue->一个动作

按价值和移动版本进行通过:

  • 通过了lvalue->一个副本,一个动作
  • 通过rvalue->两个动作

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

这取决于您如何推断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&&是实际的rvalue。


,但对于问题的理论列表就足够了。你有一个具体的。

最后,这可能是想要的:

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参数的初始化给您的构造函数,这很好。