9

Consider the following code, compiled with g++ 7.0.1 (-std=c++17):

#include <map>
#include <tuple>

int main()
{
    // Create an alias for a tuple of three ints
    using ThreeTuple=std::tuple<int,int,int>;
    // Create an alias for a map of tuple to tuple (of three ints)
    using MapThreeTupleToThreeTuple=std::map<ThreeTuple,ThreeTuple>;

    MapThreeTupleToThreeTuple m;

    // The following does NOT compile
    m.emplace({1,2,3},{4,5,6});

    // ..., and neither does this
    m.emplace(std::piecewise_construct,{1,2,3},{4,5,6});
}

I would have thought that the initializer_list arguments to map::emplace() would have sufficed and would have resulted in the insertion of the tuple key to tuple value association as specified. Apparently, the compiler disagrees.

Of course creating a tuple explicitly (i.e., ThreeTuple{1,2,3} instead of just {1,2,3}) and passing that to map::emplace() solves the problem, but why can't the initializer lists be passed directly to map::emplace() which would automatically forward them to the tuple constructors?

Michael Goldshteyn
  • 71,784
  • 24
  • 131
  • 181

3 Answers3

10

AFAIK, no changes in C++17 matter in this context. As explained by NathanOliver and Barry, {1,2,3} cannot be deduced to have any type and hence cannot be matched against a template argument. You must provide the arguments for the constructor of ThreeTuple as deducible types, i.e.

m.emplace(std::piecewise_construct,
          std::forward_as_tuple(1,2,3),
          std::forward_as_tuple(4,5,6));

which calls the constructor

template<typename T1, typename T2>
template<typename... Args1, typename... Args2 >
std::pair<T1,T2>::pair(std::piecewise_construct_t,
                       std::tuple<Args1...>, std::tuple<Args2...>);

In this particular case, you can even omit the std::piecewise_construct

m.emplace(std::forward_as_tuple(1,2,3),
          std::forward_as_tuple(4,5,6));

or (in C++17 as pointed out by Nicol in a comment)

m.emplace(std::tuple(1,2,3), std::tuple(4,5,6));

which are equivalent to

m.emplace(ThreeTuple(1,2,3), ThreeTuple(4,5,6));

and call the constructor

template<typename T1, typename T2>
std::pair<T1,T2>::pair(const&T1, const&T2);

Note also that AFAIK you cannot get this working by using std::initializer_list<int> explicitly. The reason is simply that there is not suitable constructor for pair<ThreeTuple,ThreeTuple> (the value_type of your map).

Walter
  • 44,150
  • 20
  • 113
  • 196
  • Have a +1 for showing a working example of how to do this properly. – Michael Goldshteyn May 24 '17 at 15:48
  • FYI: since the OP is using C++17, you could also use template class deduction and therefore just use `std::tuple(1, 2, 3)`. Assuming the compiler supports it and the standard library properly implements it. – Nicol Bolas May 24 '17 at 15:55
  • @NicolBolas Correct, but that is almost the same as `ThreeTuple(1,2,3)`, which the OP has mentioned. Thanks anyway, I changed the answer. – Walter May 24 '17 at 15:57
  • Even before C++17, there's nothing to be gained from using `forward_as_tuple` here compared to `make_tuple`. – T.C. May 24 '17 at 22:07
6

but why can't the initializer lists be passed directly to map::emplace()

Because initializer lists aren't expressions and so they don't have types. The signature for emplace() is just:

template< class... Args >
std::pair<iterator,bool> emplace( Args&&... args );

and you can't deduce a type from {1,2,3}. You couldn't in C++11 and you still can't in C++1z. The only exception to this rule is if the template parameter is of the form std::initializer_list<T> where T is a template parameter.

In order for m.emplace({1,2,3},{4,5,6}); to work, you'd need a signature like:

std::pair<iterator,bool> emplace(key_type&&, mapped_type&&);
Barry
  • 286,269
  • 29
  • 621
  • 977
  • What about the version with `std::piecewise_construct`? Same problem? – Michael Goldshteyn May 24 '17 at 15:41
  • @MichaelGoldshteyn Yeah. It's still just the one `emplace()` function template. – Barry May 24 '17 at 15:41
  • 2
    @MichaelGoldshteyn Do note that there are `initializer_list` and there are *initializer list*. A `initializer_list` can be constructed from a *initializer list* as it knows what the type should be. A plain template type does not so it fails. – NathanOliver May 24 '17 at 15:44
  • @NathanOliver, interesting, I thought that my `{1,2,3}` argument (would have automatically) resulted in an `std::initializer_list` parameter being deduced. – Michael Goldshteyn May 24 '17 at 15:46
  • @MichaelGoldshteyn That is only when using `auto` as it is *special*. See: https://stackoverflow.com/questions/25612262/why-does-auto-x3-deduce-an-initializer-list – NathanOliver May 24 '17 at 15:48
0

Something like that would work in C++17:

m.try_emplace({1,2,3},4,5,6);