Disambiguating list initialization for std::vector<std::string>
Asked Answered
A

2

7

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"}?

Assurance answered 11/5, 2017 at 16:43 Comment(0)
H
6

{"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()));
}
Hackler answered 11/5, 2017 at 16:51 Comment(6)
Great this is exactly what I was looking for! Thank you.Assurance
I wish that the C++ compiler would tell you which type coercion created the ambiguity. The compiler could tell me that {"str1","str2"} could be coerced to type std::string. I see that in the case where there're lots of possible coercions, the number of coercions creating ambiguity will grow large quite fast. But still, I wish that the C++ compiler would tell you which type coercion created the ambiguity.Kunz
@AriSweedler The problem is that it would likely create countless false positives. Overloading and implicit conversion happen all the time, and it's not practical for the compiler to predict which one you intended to use and which one you didn't. If you default to a conservative stance, the compiler would just be spamming warnings about possible ambiguity.Unlimited
What I was asking for is actually already given. When the compiler yells at you for being ambiguous, it tells you what signatures it can use and the line where it happened. In this case, void fxn(str) + void fxn(vector), and fxn({st1, str2}). This actually tells me that {str1, str2} can be converted to str or vector, but I didn't know how to interpret that properly. Thus, I was asking for the signature: string::string({str1, str2}) + string::string(str), not realizing that that information was already in front of me. Oops.Kunz
I think me being wrong confused you, and you answered a different question. Because what you said seems incorrect to me. The compiler never tries to predict which conversion you want. It either does the only one possible or yells at you for being ambiguous. No one wants a warning on successful implicit coercion. I was saying that, when there's more than one and it causes the compiler to fail, it might be helpful to have the signatures of all the possible type coercions.Kunz
@AriSweedler That makes sense.You are correct, I had misinterpreted your original comment.Unlimited
A
2

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
{
};
Allantois answered 11/5, 2017 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.