3

Hi there template meta programming experts.

I'm attempting to write a (class member) function the could ideally take as an argument what might be called a type agnostic map.

Ideally something like:

foo({"Bar", 42}, {"Baz", "Blargh");

as well as:

foo({"Blargh", "Bleh"}, {"Biz", 43.6}, {"Bam", {43, 43, 43}});

The class/function should then be able to convert these arguments by calling a conversion function deduced at compile time, so it would internally end up with an std::map with strings as keys and values.

The reason is simply syntactic sugar. In my use case it would be very convenient for the caller not to have to worry about converting the arguments and getting a compile time error if a non supported type was provided.

I have made it work if the caller explicitly uses make_pair as in:

foo(std::make_pair<std::string, std::chrono::miliseconds>("Time", 42));

But naturally that is not very clean and definitely not more convenient than having the caller convert the value types to std::string herself.

I have tried creating my own std::pair like class with specializations for various value types, but my compiler (gcc) is not able to find it if I call with a brace initializer just as is the case with a standard std::map. My compiler seems to see it as an std::initializer_list even if the arguments have different types.

I have more or less come to the conclusion that what I'm trying is not possible with C++ even in the C++ 14 standard, but I'm not absolutely certain.

Does anyone have any ideas on how to solve this or are able to explain why this isn't possible if that is the case?

Thanks a lot!

EDIT

Example code:

template<typename Value, typename... Args>
void foo(const std::pair<std::string, Value>& val, Args... args)
{
  foo(args...);
}

void foo(const std::pair<std::string, int>& val) {}

void foo(){}

Calling foo() like this:

foo({"key", 42});

doesn't expand the template and works, while:

foo({"key", 42}, {"another", 53})

fails to compile with the error:

no matching function for call to ‘foo(<brace-enclosed initializer list>, <brace-enclosed initializer list>)’
kingguru
  • 33
  • 3
  • 1
    C++14 would definitely reduce much of the verbosity, e.g you may write just this `std::make_pair("Time"s, 42ms)` instead of what you've written. Much better. – Nawaz Oct 13 '14 at 19:54
  • Have you tryed implement something? – GingerPlusPlus Oct 13 '14 at 20:12
  • @Nawaz: That's actually a good point. Also, your example with time made me think that maybe conversion of anything apart from simple types might not be a very good design. It shouldn't be up to the callee to decide how to convert eg. a timestamp to a string. I still find it interesting to know if it would be possible to do. – kingguru Oct 13 '14 at 21:10
  • Doesnt template void foo(Ts... a){ bar({rest...}); } work? – CoffeDeveloper Oct 14 '14 at 08:43

2 Answers2

0

One solution I can imagine is to use variadic templates:

template<typename T, typename U, typename... Rest>
auto foo(T t, U u, Rest... rest){
    //deal with t and u,
    //and call foo(rest...) recursievely
}
GingerPlusPlus
  • 5,336
  • 1
  • 29
  • 52
  • That would work thanks, but I think have a single argument list with key/value pairs not being separated but evaluated as such would be "ugly" and definitely error prone, which is why I was looking for a way to use braced constructors. – kingguru Oct 13 '14 at 20:57
0

I guess that the following code:

template <typename... Ts>
void foo(Ts... ts) {}

foo({1,2}, {3,4});

fails to compile due to exact same reason as:

template <typename T>
void foo(T t) {}

foo({1,2});

which is that brace-enclosed list has no type at all, and hence can't be deduced.

However, knowing that brace-enclosed initializer list can be used to initialize a concrete type through non-explicit constructor, and that compiler is able to deduce the T type of std::initializer_list<T>, the following code using old-fashioned variadic list works as intended:

#include <initializer_list>

struct AgnosticMap
{
    AgnosticMap(std::nullptr_t) {}

    template <typename T, typename U>
    AgnosticMap(T t, U u) {}

    template <typename T, typename U>
    AgnosticMap(T t, std::initializer_list<U> il) {}
};

void foo(AgnosticMap a1
       , AgnosticMap a2 = nullptr
       , AgnosticMap a3 = nullptr
       , AgnosticMap a4 = nullptr)
{
}

int main()
{
    foo({"Blargh", "Bleh"}, {"Biz", 43.6}, {"Bam", {43, 43, 43}});
}

DEMO

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
  • That's very cool and very close to what I was looking, but does foo() need to be declared with a fixed number of arguments? Seems a bit unflexible. I tried using variadic template argments bun ran into the exact same problem. – kingguru Oct 14 '14 at 15:05
  • @kingguru yes, that's why I called it *old-fashioned*, you can declare any number of such parameters defaulted to *nullptr*, since they are defaulted, you don't have to use that many arguments as many parameters you have (see that I use three arguments instead of four when calling that function) – Piotr Skotnicki Oct 14 '14 at 15:08
  • @kingguru and in the [DEMO](http://coliru.stacked-crooked.com/a/8d21e1ce2247c650) you can see that I check if the argument is defaulted or not – Piotr Skotnicki Oct 14 '14 at 15:11