2

In the following code, the variable number of arguments assume a string variable in itself but it does not happen for string literals

#include <iostream>
#include <string>
#include <memory>

template <typename MyType, typename StringT=std::string, typename... ArgsT,
            typename = std::enable_if_t<std::is_constructible_v<std::string, StringT>>>
  std::shared_ptr<MyType> my_function(const StringT& name, ArgsT&&... args) {
      std::cout << "name: " << name << std::endl;
      auto t = std::make_shared<MyType>(5);
      return t;
  }
  
template <typename MyType, typename... ArgsT>
  std::shared_ptr<MyType> my_function(ArgsT&&... args) {
      auto t = my_function<MyType>("default name", std::forward<ArgsT>(args)...);
      return t;
  }

int main() {
    std::string myvar= "something";
    auto ret = my_function<int>("something");
    auto ret2 = my_function<int, std::string>(myvar);
    auto ret3 = my_function<int>(myvar);
    auto ret4 = my_function<int>(42);// 42 could be anything

    return 0;
}

Prints:

name: something
name: something
name: default name
name: default name

How to avoid this and call the first constructor in the third my_function<int>(myvar) call as well? How to print "something" in all the cases except the last one?

2
  • 3
    The question here seems to basically be "Why is template <typename MyType, typename... ArgsT> a better match than template <typename MyType, typename StringT=std::string, typename... ArgsT, typename = std::enable_if_t<std::is_constructible_v<std::string, StringT>>>?" Understand that and you should be able to work around it. Commented Feb 24, 2024 at 0:56
  • 1
    The problem is that for the call my_function<int>(myvar) the generic version my_function(ArgsT&&... args) is better match because it doesn't require a conversion to const for it's argument. That is, the first overload my_function(const StringT& name, ArgsT&&... args) requires a conversion to const for its first parameter name and hence it is worse match than the generic version my_function(ArgsT&&... args). Commented Feb 24, 2024 at 2:35

2 Answers 2

2

You are using SFINAE to enable the 1st function only if std::string can be constructed from the 1st parameter type. So, you should then use SFINAE to disable the 2nd function for the same parameter type, eg:

#include <iostream>
#include <string>
#include <memory>

template <typename MyType, typename StringT=std::string, typename... ArgsT,
            typename = std::enable_if_t<std::is_constructible_v<std::string, StringT>>>
  std::shared_ptr<MyType> my_function(const StringT& name, ArgsT&&... args) {
      std::cout << "name: " << name << std::endl;
      auto t = std::make_shared<MyType>(5);
      return t;
  }
  
template <typename MyType, typename Arg1T, typename... ArgsT,
            typename = std::enable_if_t<!std::is_constructible_v<std::string, Arg1T>>>
  std::shared_ptr<MyType> my_function(Arg1T&& arg1, ArgsT&&... args) {
      auto t = my_function<MyType>("default name", std::forward<Arg1T>(arg1), std::forward<ArgsT>(args)...);
      return t;
  }

int main() {
    std::string myvar= "something";
    auto ret = my_function<int>("something");
    auto ret2 = my_function<int, std::string>(myvar);
    auto ret3 = my_function<int>(myvar);
    auto ret4 = my_function<int>(42);// 42 could be anything

    return 0;
}

Output:

name: something
name: something
name: something
name: default name

Online Demo

If you need to handle the case where no input parameters are passed in, then simply add a 3rd overload for that case:

template <typename MyType>
  std::shared_ptr<MyType> my_function() {
      auto t = my_function<MyType>("default name");
      return t;
  }

Online Demo

Sign up to request clarification or add additional context in comments.

Comments

2

The problem is that for the call my_function<int>(myvar) the generic version my_function(ArgsT&&... args) is better match because it doesn't require a conversion to const for it's argument. That is, the first overload my_function(const StringT& name, ArgsT&&... args) requires a conversion to const for its first parameter name and hence it is worse match than the generic version my_function(ArgsT&&... args).

To show this, below is a contrived example:

#include <iostream>
template<typename T, typename... Args> void func(const T&, Args&&...)
{
std::cout << "const T& version";
}
template<typename... Args> void func(Args&&...)
{
 std::cout << "Without const version";
}
int main()
{
    func(4);   //prints Without const version
} 

Solution

One way to solve this is to pass name by value by removing the low-level const from name as shown below:

template <typename MyType, typename StringT=std::string, typename... ArgsT,
            typename = std::enable_if_t<std::is_constructible_v<std::string, StringT>>>
//------------------------------------vvvvvvv------------------------->removed const & from here
  std::shared_ptr<MyType> my_function(StringT name, ArgsT&&... args) {
      std::cout << "name: " << name << std::endl;
      auto t = std::make_shared<MyType>(5);
      return t;
  }

Working demo

2 Comments

That's interesting. Why is const creating this? Any reason it is not a better match?
@soham It is not a better match because it requires a "conversion to const" while the other one don't. See this answer that should clear things up.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.