2
votes

The following code was all tried on Coliru with its default compilation arguments

g++ -std=c++17 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

Suppose we want to make a wrapper that holds on to a 'container' of data (ex, vector, list, any data structure that holds one value that can be templated). Then later on we want to convert it to another type with a helper function convert. This function can get called a lot, it would be nice to just write convert(...), and not template it like convert<TypeToConvertTo>(...). In a non MCVE, I am able to do this but it requires absolutely ugly copy-pasta of std::function parameters and function pointer parameters... which is not good (unless its a necessary evil? hopefully not)

Also suppose we have this (note that things like std::move, explicit, copying in the convert function by std::copy() or something like that, etc).

Attempt 1: Using std::function below fails:

#include <functional>
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

using std::string;
using std::vector;

template <template<typename...> class DataContainer, typename T, typename... TArgs>
struct Wrapper {
    DataContainer<T, TArgs...> dataContainer;

    Wrapper(DataContainer<T, TArgs...> container) : dataContainer(container) { }

    // Using parameter type std::function<V(T)> fails here
    template <typename V, typename... VArgs>
    DataContainer<V, VArgs...> convert(std::function<V(T)> f) {
        DataContainer<V, VArgs...> output;
        for (T t : dataContainer)
            output.emplace_back(f(t));
        return output;
    }
};

int main() {
    vector<int> nums = {123};
    Wrapper w{nums};

    vector<string> intVec = w.convert(std::to_string);

    std::cout << intVec.front() << std::endl;
}

This does not compile, and gives the error

main.cpp: In function 'int main()':

main.cpp:37:57: error: no matching function for call to 'Wrapper >::convert()'

     vector<string> intVec = w.convert(std::to_string);

                                                     ^

main.cpp:17:36: note: candidate: 'template DataContainer Wrapper::convert(std::function) [with V = V; VArgs = {VArgs ...}; DataContainer = std::vector; T = int; TArgs = {std::allocator}]'

     DataContainer<V, VArgs...> convert(std::function<V(T)> f) {

                                ^~~~~~~

main.cpp:17:36: note: template argument deduction/substitution failed:

main.cpp:37:57: note: mismatched types 'std::function' and 'std::__cxx11::string ()(long double)' {aka 'std::__cxx11::basic_string ()(long double)'}

     vector<string> intVec = w.convert(std::to_string);

                                                     ^

main.cpp:37:57: note: mismatched types 'std::function' and 'std::__cxx11::string ()(int)' {aka 'std::__cxx11::basic_string ()(int)'}

main.cpp:37:57: note: couldn't deduce template parameter 'V'

I tried making my own standalone static function in the case that overloads are messing with it, but I get the same kind of error, saying:

mismatched types 'std::function' and 'int ()(std::__cxx11::string)' {aka 'int ()(std::__cxx11::basic_string)'}

However when I change the std::function to a function pointer with:

// Now using V(*f)(T) instead
template <typename V, typename... VArgs>
DataContainer<V, VArgs...> convert(V(*f)(T)) {
    DataContainer<V, VArgs...> output;
    for (T t : dataContainer)
        output.emplace_back(f(t));
    return output;
}

This works and now prints out

123

This gives me what I want, but now I have to specify the type on convert like .convert<TypeHere>(...) which I'd like the compiler to do for me. Is this possible?

Attempt 2:

I thought I could do what the STL library does, where it templates the function like so:

template <typename Func, typename V, typename... VArgs>
DataContainer<V, VArgs...> convert(Func f) {
    DataContainer<V, VArgs...> output;
    for (T t : dataContainer)
        output.emplace_back(f(t));
    return output;
}

but then compilation fails with:

main.cpp: In function 'int main()':

main.cpp:45:57: error: no matching function for call to 'Wrapper >::convert()'

     vector<string> intVec = w.convert(std::to_string);

                                                     ^

main.cpp:33:36: note: candidate: 'template DataContainer Wrapper::convert(Func) [with Func = Func; V = V; VArgs = {VArgs ...}; DataContainer = std::vector; T = int; TArgs = {std::allocator}]'

     DataContainer<V, VArgs...> convert(Func f) {

                                ^~~~~~~

main.cpp:33:36: note: template argument deduction/substitution failed:

main.cpp:45:57: note: couldn't deduce template parameter 'Func'

     vector<string> intVec = w.convert(std::to_string);

                                                     ^

All I want is for the line

vector<string> intVec = w.convert(std::to_string);

to work without me having to write in any templates to the right of convert.

What am I doing wrong? I can get what I want with function pointers but then it seems like I have to write a bunch of overloads like V(*f)(T), V(*f)(const &T)

Attempt 3:

Lambdas don't work unless I specify the type, meaning I have to do this:

vector<string> intVec = w.convert<string>([](int i) { return std::to_string(i); });

and I want:

vector<string> intVec = w.convert([](int i) { return std::to_string(i); });

which occurs when I don't specify the type (why can't it deduce it?)

main.cpp: In function 'int main()':

main.cpp:45:82: error: no matching function for call to 'Wrapper >::convert(main()::)'

     vector<string> intVec = w.convert([](int i) { return std::to_string(i); });

                                                                              ^

main.cpp:17:36: note: candidate: 'template DataContainer Wrapper::convert(std::function) [with V = V; VArgs = {VArgs ...}; DataContainer = std::vector; T = int; TArgs = {std::allocator}]'

     DataContainer<V, VArgs...> convert(std::function<V(T)> f) {

                                ^~~~~~~

main.cpp:17:36: note: template argument deduction/substitution failed:

main.cpp:45:82: note: 'main()::' is not derived from 'std::function'

     vector<string> intVec = w.convert([](int i) { return std::to_string(i); });

                                                                              ^

In short, all of these are done because I have to keep copy pasting every function that does this kind of thing with a function pointer and a std::function overload, and then if I want to support function arguments that are rvalues, lvalues, const lvalues... I'm getting a combinatoric explosion of functions and it's making the working solution a mess. I don't know if this is unavoidable, or if there's something I'm missing to get it to deduce the type while allowing me to pass in either a function name or lambda.

1
Well, to_string has different overload, which one do you want to use?Matthieu Brucher
What is VArgs... supposed to be?Barry
@MatthieuBrucher Even if I create my own standalone function that has no overloading (as in int f(string s) { return 0; }, my problem still exists. In retrospect std::string probably was a bad choice because people might end up focusing on the wrong thing.Water
@Barry Those would be whatever extra template parameters the container uses, like std::allocator. I needed these for it to work in my main problem since I was having problems templating things like std::vector without it. While it may possibly be not required for the code I posted, it was a requirement for the non-MCVE code to function properly.Water
@Water It doesn't do what you think it does though. Right now, VArgs... just deduces as empty since you're not providing anything.Barry

1 Answers

1
votes

This is a mistake that everyone makes, all the time:

template <typename V>
DataContainer<V> convert(std::function<V(T)> f);

I understand why people do, it's just obviously the right way to do it right? Even wrote about this at length here. The key is: deducing a std::function is always wrong. You can't deduce this, because it won't allow conversions, and even if you could deduce it, it adds overhead you don't want.

Unfortunately, there's no equivalently clean language solution to this. And by clean I mean, some way where the T and the V are so clearly tied together like in the non-solution above.

What you actually want to do is this:

template <typename F, typename V = std::invoke_result_t<F, T>>
DataContainer<V> convert(F f);

That is, we're deducing just the type - and then using a type trait to figure out what the answer is.


The problem with std::to_string is totally separate - you just cannot pass an overloaded function or function template into a function template. You always have to wrap it in a lambda.