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"}
?
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.
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 .
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.
{"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()));
}
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
{
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With