C++ Std::Tuple Order of Destruction

C++ std::tuple order of destruction

The standard doesn't specify the order of destruction for std::tuple. The fact that §20.4.1/p1 specifies that:

An instantiation of tuple with two arguments is similar to an
instantiation of pair with the same two arguments.

Similar here is not interpreted as identical and consequently it's not implied that std::tuple should have a reverse destruction order of its arguments.

Given the recursive nature of std::tuple most probable is that the order of destruction is in order with the order of its arguments.

I also base my assumptions on a bug report for GCC BUG 66699 where in the discussion my assumptions above are justified.

That said, the order of destruction for std::tuple is unspecified.

c++ structured bindings: What is the standard order of destruction?

Structured binding changes absolutely nothing about the language in terms of order of construction/destruction. Structured binding is a fiction, a linguistic shorthand that turns get<I>(unnamed_object) or unnamed_object.some_name into some_name. In this fiction, unnamed_object is the actual object which is generated by the expression and captured by the auto[] structured binding declaration.

That object works exactly like any other C++ object. The order of subobject declarations in the class (if it is a class) determines the order of construction and destruction, just like any other C++ object.

Now, when it comes to std::tuple, the order of its subobject declaration is unspecified; it is allowed to vary from implementation to implementation. Again, structured binding has nothing to do with that. So the order of destruction can be whatever your particular implementation wants it to be.

Tuple isn't being constructed in order?

std::tuple construction order is currently unspecified.

A proposal for a concrete decision on its order has been submitted to the committee but until then the order should not be relied on.

Why does libstdc++ store std::tuple elements in reverse order?

See this answer for why libc++ chose forward order. As for why libstdc++ chose reverse order, that is probably because that's how it was demonstrated in the variadics template proposal, and is the more obvious implementation.

Bonus: No. These orderings have been stable in both libraries.

Update

libc++ chose forward storage order because:

  1. It is implementable.
  2. The implementation has good compile-time performance.
  3. It gives clients of libc++ something that is intuitive and controllable, should they care about the order of the storage, and are willing to depend on it while using libc++, despite its being unspecified.

In short, the implementor of the libc++ tuple merely felt that storing the objects in the order the client (implicitly) specified was the quality thing to do.

Support for std::tuple in swig?

Our goal is to make something like the following SWIG interface work intuitively:

%module test

%include "std_tuple.i"
%std_tuple(TupleDD, double, double);

%inline %{
std::tuple<double, double> func() {
return std::make_tuple(0.0, 1.0);
}
%}

We want to use this within Python in the following way:

import test
r=test.func()
print(r)
print(dir(r))

r[1]=1234

for x in r:
print(x)

i.e. indexing and iteration should just work.

By re-using some of the pre-processor tricks I used to wrap std::function (which were themselves originally from another answer here on SO) we can define a neat macro that "just wraps" std::tuple for us. Although this answer is Python specific it should in practice be fairly simple to adapt for most other languages too. I'll post my std_tuple.i file, first and then annotate/explain it after:

// [1]
%{
#include <tuple>
#include <utility>
%}

// [2]
#define make_getter(pos, type) const type& get##pos() const { return std::get<pos>(*$self); }
#define make_setter(pos, type) void set##pos(const type& val) { std::get<pos>(*$self) = val; }
#define make_ctorargN(pos, type) , type v##pos
#define make_ctorarg(first, ...) const first& v0 FOR_EACH(make_ctorargN, __VA_ARGS__)

// [3]
#define FE_0(...)
#define FE_1(action,a1) action(0,a1)
#define FE_2(action,a1,a2) action(0,a1) action(1,a2)
#define FE_3(action,a1,a2,a3) action(0,a1) action(1,a2) action(2,a3)
#define FE_4(action,a1,a2,a3,a4) action(0,a1) action(1,a2) action(2,a3) action(3,a4)
#define FE_5(action,a1,a2,a3,a4,a5) action(0,a1) action(1,a2) action(2,a3) action(3,a4) action(4,a5)

#define GET_MACRO(_1,_2,_3,_4,_5,NAME,...) NAME
%define FOR_EACH(action,...)
GET_MACRO(__VA_ARGS__, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0)(action,__VA_ARGS__)
%enddef

// [4]
%define %std_tuple(Name, ...)
%rename(Name) std::tuple<__VA_ARGS__>;
namespace std {
struct tuple<__VA_ARGS__> {
// [5]
tuple(make_ctorarg(__VA_ARGS__));
%extend {
// [6]
FOR_EACH(make_getter, __VA_ARGS__)
FOR_EACH(make_setter, __VA_ARGS__)
size_t __len__() const { return std::tuple_size<std::decay_t<decltype(*$self)>>{}; }
%pythoncode %{
# [7]
def __getitem__(self, n):
if n >= len(self): raise IndexError()
return getattr(self, 'get%d' % n)()
def __setitem__(self, n, val):
if n >= len(self): raise IndexError()
getattr(self, 'set%d' % n)(val)
%}
}
};
}
%enddef
  1. This is just the extra includes we need for our macro to work
  2. These apply to each of the type arguments we supply to our %std_tuple macro invocation, we need to be careful with commas here to keep the syntax correct.
  3. This is the mechanics of our FOR_EACH macro, which invokes each action per argument in our variadic macro argument list
  4. Finally the definition of %std_tuple can begin. Essentially this is manually doing the work of %template for each specialisation of std::tuple we care to name inside of the std namespace.
  5. We use our macro for each magic to declare a constructor with arguments for each element of the correct type. The actual implementation here is the default one from the C++ library which is exactly what we need/want though.
  6. We use our FOR_EACH macro twice to make a member function get0, get1, getN of the correct type of each tuple element and the correct number of them for the template argument size. Likewise for setN. Doing it this way allows the usual SWIG typemaps for double, etc. or whatever types your tuple contains to be applied automatically and correctly for each call to std::get<N>. These are really just an implementation detail, not intended to be part of the public interface, but exposing them makes no real odds.
  7. Finally we need an implementation of __getitem__ and a corresponding __setitem__. These simply look up and call the right getN/setN function on the class and call that instead. We take care to raise IndexError instead of the default exception if an invalid index is used as this will stop iteration correctly when we try to iterate of the tuple.

This is then sufficient that we can run our target code and get the following output:

$ swig3.0 -python -c++ -Wall test.i && g++ -shared -o _test.so test_wrap.cxx -I/usr/include/python3.7 -m32 && python3.7 run.py
<test.TupleDD; proxy of <Swig Object of type 'std::tuple< double,double > *' at 0xf766a260> >
['__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__swig_getmethods__', '__swig_setmethods__', '__weakref__', 'get0', 'get1', 'set0', 'set1', 'this']
0.0
1234.0

Generally this should work as you'd hope in most input/output situations in Python.

There are a few improvements we could look to make:

  1. Implement repr
  2. Implement slicing so that tuple[n:m] type indexing works
  3. Handle unpacking like Python tuples.
  4. Maybe do some more automatic conversions for compatible types?
  5. Avoid calling __len__ for every get/setitem call, either by caching the value in the class itself, or postponing it until the method lookup fails?

Does std::apply provide a guarantee for order of evaluation?

I have a case where it matters that the function is first applied to the first element of the tuple, then the second, then the third, ....

Then you're using the wrong function, because std::apply does something completely unrelated to what you want. For example,

std::apply(f, std::make_tuple(1, 2))

returns

f(1, 2)

rather than trying to call f(1) and f(2) separately.

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

This is bad:

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

you are constructing a tuple of rvalue references to temporaries that get destroyed at the end of the expression, so you're left with dangling references.

The correct statement would be:

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

However, until quite recently, the above was not supported by the standard. In N4296, the wording around the relevant constructor for tuple is [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
.

So, this constructor was not participating in overload resolution because int is not implicitly convertible to A. This has been resolved by the adoption of Improving pair and tuple, which addressed precisely your use-case:

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

The new wording for this constructor is, from 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.

And is_constructible<A, int&&>::value is true.

To present the difference another way, here is an extremely stripped down tuple implementation:

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

If USE_OLD_RULES is defined, the first constructor is the only viable constructor and hence the code will not compile since D is noncopyable. Otherwise, the second constructor is the best viable candidate and that one is well-formed.


The adoption was recent enough that neither gcc 5.2 nor clang 3.6 actually will compile this example yet. So you will either need a newer compiler than that (gcc 6.0 works) or come up with a different design.

Why are constructed objects, in tuple initialization, copied?

It isn't copied it is still moved.

In your case, you're creating a temporary (MoveMe{10}) that is then used to move construct the instance of MoveMe in the tuple. After the instance of MoveMe in the tuple is move constructed, the temporary that was moved is destroyed.

You can directly construct the MoveMe object in the tuple by forwarding the arguments

std::cout << "Directly Constructed" << std::endl;
auto tuple = std::tuple<MoveMe>(10);

Which will not result in a temporary being created then destroyed.

nested std::forward_as_tuple and segmentation fault

The problem here is the statement "If temporaries are supposed to live as long as there are references to them." This is true only in limited circumstances, your program isn't a demonstration of one of those cases. You are storing a tuple containing references to temporaries that are destroyed at the end of the full expression. This program demonstrates it very clearly (Live code at Coliru):

struct foo {
int value;
foo(int v) : value(v) {
std::cout << "foo(" << value << ")\n" << std::flush;
}
~foo() {
std::cout << "~foo(" << value << ")\n" << std::flush;
}
foo(const foo&) = delete;
foo& operator = (const foo&) = delete;
friend std::ostream& operator << (std::ostream& os,
const foo& f) {
os << f.value;
return os;
}
};

template <typename A, typename B>
struct node { A a; B b; };

template <typename... A>
node <A&&...> make(A&&... a)
{
return node <A&&...>{std::forward <A>(a)...};
}

template <typename N>
auto fst(N&& n)
-> decltype((std::forward <N>(n).a))
{ return std::forward <N>(n).a; }

template <typename N>
auto snd(N&& n)
-> decltype((std::forward <N>(n).b))
{ return std::forward <N>(n).b; }

int main() {
using namespace std;
// A: works fine (prints '2')
cout << fst(snd(make(foo(3), make(foo(2), foo(0))))) << endl;

// B: fine in Clang, segmentation fault in GCC with -Os
auto z = make(foo(3), make(foo(2), foo(0)));
cout << "referencing: " << flush;
cout << fst(snd(z)) << endl;
}

A works fine because it accesses the references stored in the tuple in the same full expression, B has undefined behavior since it stores the tuple and accesses the references later. Note that although it may not crash when compiled with clang, it's clearly undefined behavior nonetheless due to accessing an object after the end of its lifetime.

If you want to make this usage safe, you can quite easily alter the program to store references to lvalues, but move rvalues into the tuple itself (Live demo at Coliru):

template <typename... A>
node<A...> make(A&&... a)
{
return node<A...>{std::forward <A>(a)...};
}

Replacing node<A&&...> with node<A...> is the trick: since A is a universal reference, the actual type of A will be an lvalue reference for lvalue arguments, and a non-reference type for rvalue arguments. The reference collapsing rules work in our favor for this usage as well as for perfect forwarding.

EDIT: As for why the temporaries in this scenario don't have their lifetimes extended to the lifetime of the references, we have to look at C++11 12.2 Temporary Objects [class.temporary] paragraph 4:

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is when a default constructor is called to initialize an element of an array. If the constructor has one or more default arguments, the destruction of every temporary created in a default argument is sequenced before the construction of the next array element, if any.

and the much more involved paragraph 5:

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

  • A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the
    constructor exits.

  • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of
    the full-expression containing the call.

  • The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not
    extended; the temporary is destroyed at the end of the full-expression in the return statement.

  • A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression containing the new-initializer. [ Example:

struct S { int mi; const std::pair<int,int>& mp; };
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} }; // Creates dangling reference

—end example ] [ Note: This may introduce a dangling reference, and implementations are encouraged to issue a warning in such a case. —end note ]

The destruction of a temporary whose lifetime is not extended by being bound to a reference is sequenced before the destruction of every temporary which is constructed earlier in the same full-expression. If the lifetime of two or more temporaries to which references are bound ends at the same point, these temporaries are destroyed at that point in the reverse order of the completion of their construction. In addition, the destruction of temporaries bound to references shall take into account the ordering of destruction of objects
with static, thread, or automatic storage duration (3.7.1, 3.7.2, 3.7.3); that is, if obj1 is an object with the same storage duration as the temporary and created before the temporary is created the temporary shall be destroyed before obj1 is destroyed; if obj2 is an object with the same storage duration as the temporary and created after the temporary is created the temporary shall be destroyed after obj2 is destroyed. [ Example:

struct S {
S();
S(int);
friend S operator+(const S&, const S&);
~S();
};
S obj1;
const S& cr = S(16)+S(23);
S obj2;

the expression S(16) + S(23) creates three temporaries: a first temporary T1 to hold the result of the expression S(16), a second temporary T2 to hold the result of the expression S(23), and a third temporary T3 to hold the result of the addition of these two expressions. The temporary T3 is then bound to the reference cr. It is unspecified whether T1 or T2 is created first. On an implementation where T1 is created before T2, it is guaranteed that T2 is destroyed before T1. The temporaries T1 and T2 are bound to the reference parameters of operator+; these temporaries are destroyed at the end of the full-expression containing the call to operator+. The temporary T3 bound to the reference cr is destroyed at the end of cr’s lifetime,
that is, at the end of the program. In addition, the order in which T3 is destroyed takes into account the destruction order of other objects with static storage duration. That is, because obj1 is constructed before T3, and T3 is constructed before obj2, it is guaranteed that obj2 is destroyed before T3, and that T3 is destroyed before obj1. —end example ]

You are binding a temporary "to a reference member in a constructor's ctor-initializer".



Related Topics



Leave a reply



Submit