Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disambiguating list initialization for std::vector<std::string>

I have an overloaded function in my code with the type signatures:

void foo(std::string);
void foo(std::vector<std::string>);

I would like the user of foo to be able to call it with either a string or a list of strings

//Use case 1
foo("str");

//Use case 2
foo({"str1","str2","str3"});
foo({"str1","str2","str3","str4"});

The problem is when the caller passes in two strings into the initializer list for foo.

//Problem!
foo({"str1","str2"});

This call to foo is ambiguous because it matches both type signatures. This is because apparently {"str1","str2"} is a valid constructor for std::string

So my question is is there anything I can do in the declaration or implementation of foo such that I maintain the API I described above without hitting this ambiguous constructor case.

I do not want to define my own string class, but I am okay with defining something else instead of vector<string> as long is it can be initialized with an initializer list of strings.

Only out of curiosity, why does the string constructor accept {"str1","str2"}?

like image 309
Ross Avatar asked May 11 '17 16:05

Ross


People also ask

How do you initialize a list in C++?

Initializer List is used in initializing the data members of a class. The list of members to be initialized is indicated with constructor as a comma-separated list followed by a colon. Following is an example that uses the initializer list to initialize x and y of Point class.

What is std :: initializer list?

An object of type std::initializer_list<T> is a lightweight proxy object that provides access to an array of objects of type const T .

What is braced init list in C++?

initializer_list constructorsThe initializer_list Class represents a list of objects of a specified type that can be used in a constructor, and in other contexts. You can construct an initializer_list by using brace initialization: C++ Copy. initializer_list<int> int_list{5, 6, 7}; Important.


2 Answers

{"str1","str2"} matches the std::string constructor that accepts two iterators. Constructor 6 here. It would try to iterate from the beginning of "str1" to just before the beginning of "str2" which is undefined behavior.

You can solve this ambiguity by introducing an overload for std::initializer_list<const char*> which forwards to the std::vector<std::string> overload.

void foo(std::string);
void foo(std::vector<std::string>);

void foo(std::initializer_list<const char*> p_list)
{
    foo(std::vector<std::string>(p_list.begin(), p_list.end()));
}
like image 116
François Andrieux Avatar answered Sep 24 '22 00:09

François Andrieux


You could change your API slightly by using a variadic template, which prevents the ambiguity you're encountering.

template <typename... Ts>
auto foo(Ts...) 
    -> std::enable_if_t<all_are_convertible_to<std::string, Ts...>, void> 
{ 
    /* ... */ 
}

Usage:

foo("aaaa");
foo("aaaa", "bbb", "cc", "d");

In C++17, all_are_convertible_to can be implemented with a fold expression (or std::conjunction):

template <typename T, typename... Ts>
inline constexpr bool are_all_convertible = 
    (std::is_convertible_v<Ts, T> && ...);

In C++11 you can implement some sort of recursive type trait as follows:

template <typename, typename...>
struct are_all_convertible_to_helper;

template <typename T, typename X, typename... Xs>
struct are_all_convertible_to_helper<T, X, Xs...>
    : std::integral_constant<bool,
         std::is_convertible<X, T>::value && are_all_convertible_to_helper<T, Xs...>::value
    >
{
};

template <typename T>
struct are_all_convertible_to_helper<T> : std::true_type
{
};
like image 24
Vittorio Romeo Avatar answered Sep 22 '22 00:09

Vittorio Romeo