【问题标题】:std::move or std::forward when assigning universal constructor to member variable in C++将通用构造函数分配给 C++ 中的成员变量时的 std::move 或 std::forward
【发布时间】:2017-04-17 12:41:33
【问题描述】:

考虑以下类 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的构造函数而代表初始化成员变量foo1&lt;T&gt;的正确方法?即使用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 既是引用类型又是值类型。

【问题讨论】:

  • 我会让知道其背后原因的人发布完整答案,但如果您正在寻找一个快速的是/否,我一直被告知您的 foo2 示例是正确。
  • 这些不是“通用”(转发)引用——如果你想要一个转发引用,你需要一个模板化的构造函数。
  • 这里没有作业。
  • @Lightness Races in Orbit 我已经更新了我的措辞,谢谢!

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


【解决方案1】:

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

转发引用只在存在类型推导的情况下形成,但foo1foo2的构造函数中的T&amp;&amp;没有推导,所以它只是一个右值引用。

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

对于完美的转发版本:

  • 传递左值 -> 一份
  • 通过右值 -> 一招

对于传值和移动版本:

  • 传左值 -> 一份,一招
  • 通过右值 -> 两步

考虑此代码需要的性能以及需要对其进行多少更改和维护,并据此选择一个选项。

【讨论】:

  • "在这种情况下,你不应该在 foo1 中使用 std::move,如果构造函数传递了一个非常量的左值,它将无法编译。" 为什么?
  • 感谢您的回答,这是否意味着在这种情况下我也会使用std::movetemplate &lt;class T&gt; auto make_foo1(T&amp;&amp; t) { return foo1&lt;T&gt;{ std::move(t) }; },因为没有类型扣除?
  • @keith 我不确定你的意思; T 是从 make_foo 中的函数参数推导出来的。在这种情况下,您应该使用std::forward&lt;T&gt;(t)
  • 如果我错了,请纠正我,但从技术上讲,按值传递和移动版本,用于临时右值(在该行上构造的东西,例如按值返回函数的结果,直接构造函数调用等;我的命名法很弱,但我认为这可能被描述为prvalue?),RVO can be used to construct directly into t,对吧?因此,在 RVO 的情况下,按值传递和移动的 r 值情况也可能只是一个移动。平均而言仍然较慢(对于三种可能的情况,平均增加 0.66 步),但它匹配一种情况。
  • 如果T作为引用类型是有效的,那么你应该forward
【解决方案2】:

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

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

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

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

上述代码在构造函数中从bob 静默移动到std::vector&lt;int&gt;&amp;foo1&lt;std::vector&lt;int&gt;&amp;&gt;

foo2 做同样的事情会奏效。你会得到一个foo2&lt;std::vector&lt;int&gt;&amp;&gt;,它会引用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&amp;&amp; 是实际的右值。


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

最后这可能是你想要的:

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 参数,这很好。

【讨论】:

    猜你喜欢
    • 2014-04-15
    • 2016-03-21
    • 2016-07-08
    • 2017-01-31
    • 2015-05-03
    • 1970-01-01
    • 2013-12-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多