std::tuple 对于不可复制和不可移动的对象

std::tuple for non-copyable and non-movable object

我有一个 class 删除了复制和移动 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;
};

我想用这个 class 的对象创建一个元组。但以下不编译:

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。那么,销毁对象元组的意义是什么?

这很糟糕:

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

Requires: sizeof...(Types) == sizeof...(UTypes). is_constructible<Ti, Ui&&>::value is true for all i.
Effects: Initializes the elements in the tuple with the corresponding value in std::forward<UTypes>(u).
Remark: This constructor shall not participate in overload resolution unless each type in UTypes is implicitly convertible to its corresponding type in Types.

因此,此构造函数未参与重载决策,因为 int 无法隐式转换为 A。这已通过采用 Improving pair and tuple 得到解决,它恰好解决了您的用例:

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

此构造函数的新措辞来自 N4527:

Remarks: This constructor shall not participate in overload resolution unless sizeof...(Types) >= 1 and is_constructible<Ti, Ui&&>::value is true for all i. The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for at least one i.

并且is_constructible<A, int&&>::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 可以工作),要么想出一个不同的设计。

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

因此 auto q = std::tuple<A&&,A&&>(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
}