【问题标题】:std::tuple for non-copyable and non-movable objectstd::tuple 用于不可复制和不可移动的对象
【发布时间】:2015-09-24 13:47:26
【问题描述】:

我有一个删除了复制和移动 ctor 的课程。

struct A
{
    A(int a):data(a){}
    ~A(){ std::cout << "~A()" << this << " : " << data << std::endl; }

    A(A const &obj) = delete;
    A(A &&obj) = delete;

    friend std::ostream & operator << ( std::ostream & out , A const & obj);

    int data;
};

我想用这个类的对象创建一个元组。但以下内容无法编译:

auto p = std::tuple<A,A>(A{10},A{20}); 

另一方面,以下确实编译,但输出令人惊讶。

int main() {
    auto q = std::tuple<A&&,A&&>(A{100},A{200});
    std::cout << "q created\n";
}

输出

~A()0x22fe10 : 100
~A()0x22fe30 : 200
q created

这意味着一旦元组构造线结束,就会调用对象的 dtor。那么,销毁对象的元组有什么意义呢?

【问题讨论】:

  • std::tuple&lt;A&amp;&amp;,A&amp;&amp;&gt;(A{100},A{200}); 是一个悬挂引用的元组。我认为std::tuple&lt;A,A&gt; p(100, 200); 应该可以代替
  • @PiotrSkotnicki 它不能在 clang 或 g++ 上编译:coliru.stacked-crooked.com/a/1fe2146b7881e241
  • @NathanOliver 确实如此,使用 libc++ 或更新的 libstdc++
  • 我认为你可以用一对和std::piecewise_construct 做到这一点。 AFAIK tuple 没有采用std::piecewise_construct_t 的构造函数。
  • @Yakk 该构造函数将是explicit,因为int const&amp; 不能转换为A

标签: c++ c++11 tuples


【解决方案1】:

这很糟糕:

auto q = std::tuple<A&&,A&&>(A{100},A{200});

您正在构建一个tuple 的右值引用,这些引用指向在表达式末尾被销毁的临时对象,因此您留下了悬空引用。

正确的说法是:

std::tuple<A, A> q(100, 200);

但是,直到最近,标准还不支持上述内容。在N4296中,tuple的相关构造函数周围的措辞是[tuple.cnstr]:

template <class... UTypes>
  constexpr explicit tuple(UTypes&&... u);

需要sizeof...(Types) == sizeof...(UTypes)is_constructible&lt;Ti, Ui&amp;&amp;&gt;::value 是真的 对于所有i
效果:用std::forward&lt;UTypes&gt;(u)中的相应值初始化元组中的元素。
备注:此构造函数不得参与在重载决议中除非UTypes 中的每种类型都是 隐式转换为Types中的对应类型

因此,此构造函数没有参与重载解析,因为 int 不能隐式转换为 A。采用Improving pair and tuple 解决了这个问题,它正好解决了您的用例:

struct D { D(int); D(const D&) = delete; };    
std::tuple<D> td(12); // Error

这个构造函数的新措辞是,来自 N4527:

备注:此构造函数不应参与重载决议除非sizeof...(Types) &gt;= 1is_constructible&lt;Ti, Ui&amp;&amp;&gt;::value 对所有i 都为真。当且仅当构造函数是显式的 如果is_convertible&lt;Ui&amp;&amp;, Ti&gt;::valuefalse 至少有一个i

is_constructible&lt;A, int&amp;&amp;&gt;::value 是真的。

为了以另一种方式呈现差异,这里是一个极其精简的元组实现:

struct D { D(int ) {} D(const D& ) = delete; };

template <typename T>
struct Tuple {
    Tuple(const T& t)
    : T(t)
    { }

    template <typename U,
#ifdef USE_OLD_RULES
              typename = std::enable_if_t<std::is_convertible<U, T>::value>
#else
              typename = std::enable_if_t<std::is_constructible<T, U&&>::value>
#endif
              >
    Tuple(U&& u)
    : t(std::forward<U>(u))
    { }

    T t;
};

int main()
{
    Tuple<D> t(12);
}

如果定义了USE_OLD_RULES,则第一个构造函数是唯一可行的构造函数,因此代码将无法编译,因为D 是不可复制的。否则,第二个构造函数是最可行的候选者,并且该构造函数格式良好。


采用太新了,以至于 gcc 5.2 和 clang 3.6 实际上都不会编译这个示例。因此,您要么需要一个比它更新的编译器(gcc 6.0 有效),要么提出不同的设计。

【讨论】:

  • 我认为std::tuple&lt;D&gt; td = std::make_tuple(12); 也应该有效吗?
  • 不是 C++11 中的 template &lt;class... UTypes&gt; explicit tuple(UTypes&amp;&amp;... args); 构造函数吗?
  • @Yakk 不,那个构造函数是explicit。但是std::tuple&lt;D&gt; td{std::make_tuple(12)} 在那篇论文被采纳之前也行不通。
  • @PiotrSkotnicki 查看更新以了解为什么没有调用它。
  • "这个构造函数没有参与重载解析,因为 int 不能隐式转换为 A" 没有引用问题中的任何内容,因为问题没有t 在任何地方使用任何隐式转换。请注意,它使用 std::tuple&lt;A&amp;&amp;,A&amp;&amp;&gt;(A{100},A{200}) 而不是 std::tuple&lt;A&amp;&amp;,A&amp;&amp;&gt;(100,200); 所以这是一个无关紧要的点,讨论 N4296 引用的整个段落也是如此。
【解决方案2】:

你的问题是你明确要求一个右值引用的元组,而右值引用离指针并不远。

所以auto q = std::tuple&lt;A&amp;&amp;,A&amp;&amp;&gt;(A{100},A{200}); 创建了两个 A 对象,获取对它们的(右值)引用,使用引用构建元组...并销毁临时对象,留下两个悬空引用

即使据说它比旧的 C 及其悬空指针更安全,C++ 仍然允许程序员编写错误的程序。

无论如何,以下是有意义的(注意使用 A& 而不是 A&&):

int main() {
    A a(100), b(100); // Ok, a and b will leave as long as main
    auto q = tuple<A&, A&>(a, b);  // ok, q contains references to a and b
    ...
    return 0; // Ok, q, a and b will be destroyed
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-04-08
    • 1970-01-01
    • 1970-01-01
    • 2014-05-24
    • 1970-01-01
    • 1970-01-01
    • 2023-04-10
    • 2020-10-03
    相关资源
    最近更新 更多