Search code examples
c++templatesc++17template-argument-deduction

Template parameter can't be deduced on implicitly constructed argument


I would like to have the following code in c++17:

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

class Foo;

template<class T>
class Bar {
public:

    std::function<T(Foo&)> m_fn;

    template<class Fn>
    Bar(Fn fn) : m_fn(fn) {};

    T thing(Foo &foo) const {
        return m_fn(foo);
    }
};


template<class Fn>
Bar(Fn) -> Bar<decltype(std::invoke(std::declval<Fn>(),
                                    std::declval<Foo&>()))>;

class Foo {
public:
    Foo() {};

    template<class T>
    std::vector<T> do_thing(const Bar<T> &b) {
        std::vector<T> r;

        r.push_back(b.thing(*this));

        return r;
    }
};


std::string test(Foo &) {
    return "hello";
}

int main() {
    Foo foo = Foo();

    // works
    std::vector<std::string> s = foo.do_thing(Bar{test});
    
    // cant deduce T parameter to do_thing
    std::vector<std::string> s = foo.do_thing({test});
}

But compiling this gives me "couldn't deduce template parameter ‘T’" on the call to do_thing. Having do_thing(Bar{test}) fixes this and works fine but equates to some ugly code in the real code equivalent. I would like to have do_thing({test}) or do_thing(test) implicitly construct a Bar and pass that as the argument if possible.

I also don't want to forward declare a variable to pass into do_thing either

Is there some way to guide the inference of template argument T so that the call to do_thing can stay clean?

Edit:

Sorry for the late edit, but the arguments to the Bar constructor are over simplified in the example I included. In reality, there is an extra parameter std::optional<std::string> desc = std::nullopt and that might change in the future (although unlikely). So constructing the Bar inside do_thing would be a bit hard to maintain...


Solution

  • would like to have do_thing({test}) or do_thing(test) implicitly construct a Bar and pass that as the argument if possible.

    Unfortunately, when you call do_thing({test}) or do_thing(test), test (or {test}) isn't a Bar<T> object. So the compiler can't deduce the T type and can't construct a Bar<T> object.

    A sort of chicken-and-egg problem.

    The best I can imagine is to add, in Foo, a do_test() method as follows

    template<typename T>
    auto do_thing (T const & t)
     { return do_thing(Bar{t}); } 
    

    This way you can call (without graphs)

    std::vector<std::string> s = foo.do_thing(test);
    

    You get the same result as

    std::vector<std::string> s = foo.do_thing(Bar{test});
    

    -- EDIT --

    The OP ask

    is there any way of preserving the {test} brace syntax? maybe with initializer_list or something?

    Yes... with std::initializer_list

    template<typename T>
    auto do_thing (std::initializer_list<T> const & l)
    { return do_thing(Bar{*(l.begin())}); }
    

    but, this way, you accept also

    std::vector<std::string> s = foo.do_thing(Bar{test1, test2, test3});
    

    using only test1

    Maybe a little better... another way can be through a C-style array

    template <typename T>
    auto do_thing (T const (&arr)[1])
     { return do_thing(arr[0]); }
    

    This way you accept only an element.