23

I've been playing around with variadic templates and noticed the following.

This works fine:

auto t = std::make_tuple(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16);

This will give the error (gcc 4.8.2 (edit: Clang 3.4) has maximum depth of 256 by default):

auto t2 = std::make_tuple(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17);

However, creating the tuple directly will work:

std::tuple<int,int,int,int,int,int,int,int,int,int,int,int,int,int,int,int,int> t3(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17);

I noticed this while trying to create a templated function that returns a templated class.

template <typename...Arguments>
struct Testing {
  std::tuple<Arguments...> t;
  Testing(Arguments...args) : t(args...) {}
};

template <typename... Arguments>
Testing<Arguments...> create(Arguments... args) {
  return Testing<Arguments...>(args...);
}

In this case, this will work:

auto t4 = create(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16);

and this won't:

auto t5 = create(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17);
Constructor
  • 7,273
  • 2
  • 24
  • 66
Jordan
  • 233
  • 2
  • 6
  • 1
    Probably because `create` is a "recursive template" and when you give it 17 arguments it needs a depth of 17, and when you give it 16 arguments it needs a depth of 16, and the limit is (apperently) 16. – Mooing Duck Apr 29 '14 at 20:51
  • 4
    g++4.8.2 needs an instantiation depth of exactly 231 to compile `make_tuple` with 17 arguments. [Live example](http://coliru.stacked-crooked.com/a/67d4b91e9980f2f8) The `make_tuple` itself adds only one instantiation depth as far as I can see, the problem probably is `tuple` itself. – dyp Apr 29 '14 at 20:58
  • ooooooh, that explains my confusion – Mooing Duck Apr 29 '14 at 21:06
  • 1
    They should definitely work on making some of this template metamagic less recursive – Mooing Duck Apr 29 '14 at 21:06
  • I'm able to create a tuple with up to 242 elements when creating it directly. [Example](http://coliru.stacked-crooked.com/a/2d320e012a500bec) – Jordan Apr 29 '14 at 21:36
  • Sorry, but who wrote the example with make_tuple_ref_unwrapper ? why is it necessary ? – grisha Apr 29 '14 at 21:39
  • 1
    @user3586046 The `noexcept` check is for a move constructor, so [trying to move that tuple fails](http://coliru.stacked-crooked.com/a/5a1d1ea917343f83), even [for much fewer arguments](http://coliru.stacked-crooked.com/a/07c703a5d64d796b) – dyp Apr 29 '14 at 21:41

1 Answers1

13

The problem is not make_tuple, but the move constructor of tuple in libstdc++ (gcc4.8.2).

For class templates, the member functions are only instantiated when used. The noexcept-specification is delayed similarly, see e.g. CWG issue 1330.

When initialising a variable from make_tuple, the move constructor is instantiated, even if it is elided (e.g. to check if it is ill-formed). That's why you see a difference between just defining a tuple variable and using make_tuple.

The move constructor has a conditional noexcept that is implemented recursively. Therefore, for each template argument, a constant number of additional instantiations is required. An excerpt of clang++'s error output when exceeding the max instantiation depth: (brace yourself, wall of text incoming)

/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:803:24: note: in instantiation of default argument for '__test, std::_Tuple_impl &&>' required here
      static true_type __test(int);
                       ^~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:803:24: note: while substituting deduced template arguments into function template '__test' [with _Tp = std::_Tuple_impl, _Arg = std::_Tuple_impl &&, $2 = ]
      static true_type __test(int);
                       ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:117:14: note: in instantiation of template class 'std::__is_direct_constructible_impl, std::_Tuple_impl &&>' requested here
    : public conditional::type
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:818:14: note: in instantiation of template class 'std::__and_ >, std::__is_direct_constructible_impl, std::_Tuple_impl &&> >' requested here
    : public __and_,
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:896:14: note: in instantiation of template class 'std::__is_direct_constructible_new_safe, std::_Tuple_impl &&>' requested here
    : public conditional::value,
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:904:39: note: in instantiation of template class 'std::__is_direct_constructible_new, std::_Tuple_impl &&>' requested here
    : public integral_constant, std::_Tuple_impl &&>' requested here
    : public __is_direct_constructible
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:956:39: note: in instantiation of template class 'std::__is_constructible_impl, std::_Tuple_impl &&>' requested here
    : public integral_constant, std::_Tuple_impl &&>' requested here
    : public conditional::type
                         ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:1042:14: note: in instantiation of template class 'std::__and_, std::_Tuple_impl &&>, std::__is_nt_constructible_impl, std::_Tuple_impl &&> >' requested here
    : public __and_,
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:1073:14: note: in instantiation of template class 'std::is_nothrow_constructible, std::_Tuple_impl &&>' requested here
    : public is_nothrow_constructible
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:1079:14: note: in instantiation of template class 'std::__is_nothrow_move_constructible_impl, false>' requested here
    : public __is_nothrow_move_constructible_impl
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:117:14: note: in instantiation of template class 'std::is_nothrow_move_constructible >' requested here
    : public conditional::type
             ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/tuple:268:16: note: in instantiation of template class 'std::__and_, std::is_nothrow_move_constructible > >' requested here
      noexcept(__and_,
               ^
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/type_traits:802:24: note: in instantiation of exception specification for '_Tuple_impl' requested here
             = decltype(::new _Tp(declval()))>

We can see here the implementation e.g. of is_nothrow_move_constructible in terms of is_nothrow_constructible which is implemented in terms of __is_nt_constructible and so on, for 15 instantiation levels. This is printed like a call stack, so you can follow the instantiations by starting from the bottom.


This means that each template argument for tuple requires 15 additional instantiation levels for this check. On top of that, 9 levels are always required (constant depth).

Therefore, 17 arguments require an instantiation depth of 17*15+9 == 264.

dyp
  • 38,334
  • 13
  • 112
  • 177
  • 2
    C++11 recommends a maximum instantiation depth of at least 1024, g++ has a default maximum depth of 900. See the entry for `-ftemplate-depth` in the [g++4.8.2 manual](http://gcc.gnu.org/onlinedocs/gcc-4.8.2/gcc/C_002b_002b-Dialect-Options.html#C_002b_002b-Dialect-Options). – dyp Apr 29 '14 at 23:55
  • Is there a way to circumvent this ? I mean could one get [this](http://coliru.stacked-crooked.com/a/de977996dc0f2d6c) to compile w/o increasing the instantiation depth? – Lorah Attkins Mar 26 '16 at 22:18
  • Or even maybe a different tuple implementation that does suffer from such problems? (I've checked boost::tuple and it fails as well) – Lorah Attkins Mar 26 '16 at 22:25
  • 1
    @LorahAttkins It should be possible to reduce the amount of template instantiations per tuple element by implementing the check differently (like inlining), and maybe one could apply techniques to reduce the instantiation *depth* (while keeping the amount of instantiations constant or even increasing them, i.e. flattening the instantiations). Both approaches would require using a different `std::tuple` implementation. For the libstdc++ implementation, maybe it is short-circuited so you could bail out the instantiations by providing a non-noexcept-move-constructible tuple element type. – dyp Mar 27 '16 at 01:11