【问题标题】:Generic copy-constructor with rvalue-reference members具有右值引用成员的通用复制构造函数
【发布时间】:2013-09-26 23:46:30
【问题描述】:

我正在开发一个简单的包装模板类,它会在调用特殊成员函数时进行记录。这些函数不能默认,因为它们执行额外的日志相关任务。

template <typename T>
struct logger {
    logger(T const& value) : value_(value) { /*...log...*/ }
    logger(T&& value) : value_(std::move(value)) { /*...log...*/ }
    logger(logger const& other) : value_(other.value_) { /*...log...*/ }
    logger(logger&& other) : value_(std::move(other.value_)) { /*...log...*/ }

    T value_;
};

不幸的是,当包装类型是 rvalue-reference 时,copy-constructor 无法编译并显示以下错误消息:

错误:无法将‘int’左值绑定到‘int&&’

原因是隐式复制构造函数对于 rvalue-reference 成员的行为会有所不同:

[class.copy 12.8/15] 非联合类X 的隐式定义的复制/移动构造函数执行其基类和成员的成员复制/移动。 [...]x 成为构造函数的参数,或者对于移动构造函数来说,是一个引用参数的xvalue。每个基本或非静态数据成员都以适合其类型的方式复制/移动:

  • 如果成员是数组,每个元素直接用x的对应子对象初始化;
  • 如果成员m有右值引用类型T&amp;&amp;,则直接用static_cast&lt;T&amp;&amp;&gt;(x.m)初始化;
  • 否则,基址或成员将直接使用x 的相应基址或成员进行初始化。

这让我想到了我的问题:如何编写一个通用的 copy-constructor,即使使用 rvalue-references 作为成员。

对于这种特殊情况,我可以为 rvalue-references 添加一个额外的特化。但是,我正在寻找一种不限于单个成员并且不会引入代码重复的通用解决方案。

【问题讨论】:

  • 你不能不做logger(logger const&amp; other) : value_(static_cast&lt;T&gt;(other.value_))吗?因为Tint&amp;&amp;
  • Kal: Tint&amp;&amp; 仅在一种特殊情况下,在另一种情况下它可能是一个巨大的对象,这会创建一个不必要的副本。
  • 如果T 是右值引用,你想做什么?在这种情况下,您没有值语义。在这种情况下,也应该禁用移动 ctor,以避免生命周期问题。
  • @DyP:我想做与隐式复制构造函数相同的操作。也就是说,传播引用。
  • 对于右值引用数据成员,默认隐式复制ctor被删除。

标签: c++ c++11 rvalue-reference


【解决方案1】:

这里是龙。

logger(logger const& other) : value_(other.value_)

表达式other.value_T const 类型的左值,例如int&amp;int&amp;&amp;int const

  1. 如果T == int&amp;&amp;,你需要做一个move,因为表达式是一个左值。 move 相当于static_cast&lt;int&amp;&amp;&gt;,所以你也可以直接使用static_cast

  2. 如果T == int&amp;,则不需要强制转换。

  3. 如果T == int,则不需要强制转换。

对于定义为的复制ctor:

logger(logger const& other) : value_(static_cast<T>(other.value_)) {/*...*/}

应用于第三种情况,这被定义为引入一个临时的,并且可能导致额外的复制/移动,尽管我认为它可以被省略。

不依赖复制/移动省略的解决方案是引入weird_cast,在任何情况下都会产生所需的类型:

#include <type_traits>

template<class T, class U>
typename std::enable_if<std::is_reference<T>{}, T>::type
weird_cast(U& p)
{
    return static_cast<T>(p);
}

template<class T, class U>
typename std::enable_if<not std::is_reference<T>{}, T const&>::type
weird_cast(U const& p)
{
    return p;
}

int main()
{
    int           o = 42;
    int &        lo = o;
    int &&       ro = std::move(o);
    int const   lco = o;

    int&& r = weird_cast<int&&>(ro);
    int&  l = weird_cast<int& >(lo);
    int   d = weird_cast<int  >(lco);
}

这类似于std::forward,但也支持“转发”非引用类型。


龙在哪里?

[class.copy]/11 指定:

如果X 具有以下属性,则类X 的默认复制/移动构造函数被定义为已删除:

  • [...]
  • 对于复制构造函数,右值引用类型的非静态数据成员
  • [...]

右值引用通常绑定到 xvalue 或 prvalue,即绑定到引用“接近其生命周期结束”的对象的表达式。由于生命周期不会通过函数边界延长,因此允许这样的“复制”很容易出错。

【讨论】:

  • 我只想从另一个相同类型的成员(但可能不同的值类别)初始化T 类型的成员。没有const_cast 不应该在任何情况下完成。另外,您介意添加对 12.8/11 的引用吗?
  • @K-ballo 通过将参数设为logger const&amp; other,表达式other.value_ 的类型为T const(因此,const_cast)并且是一个左值(因此,move /static_cast)。正如我在之前的评论中所写,非常量复制 ctor 允许摆脱 const_cast,但即使覆盖并不能保证乍一看不会修改参数。
  • 我还是不明白为什么需要const_cast。如果loggerconst,则成员引用仍然是非常量。没有T&amp; constT&amp;&amp; const
  • @K-ballo 嗯!你是对的,我的立场是正确的。我认为这条规则只适用于指针,而不适用于引用。
  • 为了避免给未来的读者造成混淆,我认为重要的是要注意我实际上有一个 int&amp;&amp; 类型的 lvalue,而不是 int&amp;
【解决方案2】:

你可以为右值引用写一个特化:

template<typename T>
struct logger<T&&>{
  ...
};

但实际上,我认为您不希望 logger::_value 成为右值引用...

编辑

虽然我觉得这不是一个糟糕的解决方案,因为它是所有右值引用的通用解决方法,但这里是另一个没有文字专业化的选项:

template<typename TT>
struct logger{
  typedef typename rvalue_remover<TT>::value T;
  //your previous code here
};

rvalue_remover 是这样的:

template<typename T>struct rvalue_remover{typedef T value;};
template<typename T>struct rvalue_remover<T&&>{typedef T value;};

我很确定这已经在 c++11 中定义了,但是我没有在这里安装它,所以我不记得名字了。

编辑2

啊!找到了!它被称为std::remove_reference&lt;T&gt;::type,并在#include &lt;type_traits&gt;中声明

【讨论】:

  • 他从字面上说他不想写专业,但我不知道为什么不
  • 我可以,但正如我在问题中所说,这不是针对此问题的一般答案,而是针对此特定情况的解决方法。
  • @cluracan:我不想删除 rvalue-reference。我打算保留它,并且仍然有一个 copy-constructible 类。在某些情况下,将 rvalue-references 作为成员是完全可以的。如果您正在寻找示例,请搜索 std::forward_as_tuple()
  • @K-ballo - 保留右值引用非常危险。它会导致您出现错误,因为它可能会在没有通知的情况下从其他地方被销毁。甚至复制右值引用的想法也没有多大意义。右值引用的全部意义在于它是临时的 - 整个想法是,一旦某些东西被定义为右值引用,它就再也不会被使用,因此可以随意销毁。
  • @cluracan:我知道危险,它们不适用于我的用例。此包装器用于将使用 rvalue-reference 的地方,因此记录器不会给情况增加任何危险。
猜你喜欢
  • 2012-05-09
  • 1970-01-01
  • 2013-08-07
  • 2015-11-24
  • 2017-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多