Search code examples
c++templatesc++20

Template argument deduction fail for parameter pack containing string reference


The following code example represents a simplified version of my code.

The code snippet contains a class called Command that I can construct using named constructors. When constructing the class, it requires a string that I would like to parse by reference to limit stack usage.

My production code contain many different types of the Command class that all derives from a base (omitted here for simplicity), so I have also created a templated 'instantiate' function that hides some of the std::shared_ptr boiler-plate code.

The problem I face is, that when using the templated instantiate function with a reference to a std::string, the compiler is unable to deduce the template parameters and complains about inconsistent parameter pack. In the example main function I included calling the named constructor directly with a string reference, and also an example that calls an alternative named constructor that takes just a string.

I would like to avoid the named constructor that takes a string instance, so can anyone explain to me why the named constructor that takes a string reference fails when encapsulating it in a templated function?

#include <memory>
#include <string>

class Command
{
public:
    static Command* named_constructor_str_ref(const std::string &t)
    {
        return new Command(t);
    }
    static Command* named_constructor(const std::string t)
    {
        return new Command(t);
    }
    ~Command() = default;

private: 
    explicit Command(const std::string &t) : text(t) {}
    const std::string text;
};

template <typename T, typename... Ts>
std::shared_ptr<Command> instantiate(T *(*named_ctr)(Ts...), Ts... args)
{
    if(named_ctr != nullptr)
    {
        return std::shared_ptr<T>(named_ctr(args...));
    }
    return nullptr;
}

int main(void)
{
    const std::string s = "My text";

// Calling named constructor with string reference directly works
    auto c1 = std::shared_ptr<Command>(Command::named_constructor_str_ref(s));

// Instantiating with no string reference works
    auto c2 = instantiate(Command::named_constructor, s);

// Instantiating with string reference does not work... Why????
    auto c3 = instantiate(Command::named_constructor_str_ref, s);

    return 0;
}

The error message from gcc

<source>: In function 'int main()':
<source>:45:26: error: no matching function for call to 'instantiate(Command* (&)(const std::string&), const std::string&)'
   45 |     auto c3 = instantiate(Command::named_constructor_str_ref, s);
      |               ~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:24:26: note: candidate: 'template<class T, class ... Ts> std::shared_ptr<Command> instantiate(T* (*)(Ts ...), Ts ...)'
   24 | std::shared_ptr<Command> instantiate(T *(*named_ctr)(Ts...), Ts... args)
      |                          ^~~~~~~~~~~
<source>:24:26: note:   template argument deduction/substitution failed:
<source>:45:26: note:   inconsistent parameter pack deduction with 'const std::__cxx11::basic_string<char>&' and 'std::__cxx11::basic_string<char>'
   45 |     auto c3 = instantiate(Command::named_constructor_str_ref, s);
      |               ~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Solution

  • In

    template <typename T, typename... Ts>
    std::shared_ptr<Command> instantiate(T *(*named_ctr)(Ts...), Ts... args)
    

    Ts... is deduced from 2 places:

    • T *(*named_ctr)(Ts...)
    • Ts...args

    With instantiate(Command::named_constructor_str_ref, s)

    Ts... is deduced as const std::string& from the former, and std::string from the latter, so mismatching deduction, so a fail. (It is the meaning of gcc error "inconsistent parameter pack deduction with")

    You might

    • have separate template arguments:
    template <typename T, typename... Ts, typename... Us>
    std::shared_ptr<Command> instantiate(T *(*named_ctr)(Ts...), Us&&... args)
    {
        if(named_ctr != nullptr)
        {
            return std::shared_ptr<T>(named_ctr(std::forward<Us>(args)...));
        }
        return nullptr;
    }
    // or
    template <typename Func, typename... Ts>
    std::shared_ptr<Command> instantiate(Func f, Ts&&... args)
    {
        if (f)
        {
            return std::shared_ptr<std::remove_pointer_t<decltype(f(std::forward<Ts>(args)...))>>(f(std::forward<Ts>(args)...));
        }
        return nullptr;
    }
    
    • disallow deduction
    template <typename T, typename... Ts>
    std::shared_ptr<Command> instantiate(T *(*named_ctr)(Ts...), std::type_identity_t<Ts>... args)
    {
        if(named_ctr != nullptr)
        {
            return std::shared_ptr<T>(named_ctr(std::forward<Ts>(args)...));
        }
        return nullptr;
    }