Are you the C++ experienced reader to solve the following challenge?
Given a class C (edit: covered by binary compatibility needs) with the overloaded methods foo()
and foo(A a)
:
class C
{
public:
void foo();
void foo(A a);
};
Now you want to add an overload, to serve further use-cases as requested by API consumers, and going for another overload matches your API guidelines :
void foo(B b);
But there is existing consumer code, making use of C++17, which calls
C c;
c.foo({});
So the new overload will not be source-compatible and break existing code, due to the ambiguity whether {}
should be turned into an instance of A
or B
.
What could be done about this?
The goals here would be:
- to enable API consumers to write code which works as if there are the two old and the one new overloads declared
- any calls using the
{}
expression as argument are resolved to a method which emits a compiler warning to avoid that ambiguous argument and which on any next compatibility breaking occasion can be simply removed
While asking around, some approaches have been proposed, but so far none could satisfy the second goal, catching existing ambiguous argument expressions to hint API consumers to adapt them.
Would you have an idea?
Edit: Note that we cannot change A or B (might be types/classes from elsewhere), and only can add new API to C, due to binary compatibility needs.
Edit: On a second thought, similar problems also exist before C++17 already when an argument has a type which can be implicitly converted both to A
and B
, by implicit constructors or type operator methods.
You could add the following overload:
[[deprecated]] void foo(std::initializer_list);
Thanks for the idea, but we already tested that. And found that the compiler decides to interpret {} (“empty curly braces”) as something to use for value-initialization, and does not even consider std::initializer_list, because there is no type given for the template or one derivable.
Not sure why it doesn’t work for you, maybe you underspecified the problem? https://onlinegdb.com/LwhfxxvtN demonstrates it.
Another way of lifting the ambiguity is to use templates, since they always come in consideration later than normal overloads:
[[deprecated]] void bar(std::initializer_list) {}
template< class Arg, class = std::enable_if_t< std::is_convertible_v > > void bar(Arg a);
template< class Arg, class = std::enable_if_t< std::is_convertible_v > > void bar(Arg b);
It’s certainly less readable though.
Ah, you used int with std::initializer_list, guess that had been eaten by the comment field.
Where what I had tried before was template <typename T> void foo(std::initializer_list<T>) {} which then the compiler did not pick up, due to no idea what to use for T obviously.
Promising, will investigate more and play around with that
The idea with the std::enable_if_t has the issue that (what I forgot to mention) the API also needs to be BC, so foo(A) needs to stay as is, thus cannot be moved back in the list of considerations by the template approach.
I think I would define a private class Empty{ Empty()=delete; } and take an initializer_list of Empty instead of int. Just to avoid surprising behavior if you try to pass a non-empty initializer list.
Yes, that sounds like a good idea, similar idea in shower thoughts here 🙂 And from first testing definitely something one wants to do and possibly tune some more in case there are also ambiguous non-empty {} calls possible.
I don’t think 2 second is correct from BC respective. Calling {} should be resolved to A and it will for any already compiled clients, so just adding foo(B b) at end will continue work for existing clients and will break foo({}) for new clients to well specified it should be called by A or B
I think we actually agree, but not sure I got you.
Client code with sources calling foo({}) compiled against the old version will use the symbol for C::foo(A), and not be bothered by any new symbol for C::foo(B), yes. So BC is ensured.
But once that client code is recompiled against the new API, it will fail. This is a scenario quite often seen e.g. with Linux distributions who use some released versions of client code only and rebuild e.g. with new compiler versions. And who will not be happy if suddenly SC is broken. Or developers working on something on the client code and who do not suddenly have to fix breakage elsewhere only because a newer version of a library is provided.
Correct. Distributions will not be happy, but i still think they should use the version of the client.
[…] C++17’s {} impeding SC for new method overloads | Attracted by virtual constructs […]
https://godbolt.org/z/nadefxn34
or if you can modify B:
https://godbolt.org/z/vTns69v58
Thanks. But condition is that no API can be removed, and we can only add new API to C (and cannot change A and B).
Will amend as comment o original text, as this was implied, but seemingly not obvious.
https://godbolt.org/z/85KqoK3xf
That’s the best we can do, i think
That sadly is missing to meet the second criteria: any client code using a {} expression should trigger a warning, by being resolved to a method which has e.g. [[deprecated(“explicit argument type, please”)]], And only such client code, not any calls with an argument of type A.
Ops, I misunderstood initially what you wanted to achieve.
Here is a solution: https://godbolt.org/z/43e4qz8nd
Or like this: https://godbolt.org/z/xhasdWTqc
struct C needs to keep the existing methods though, as they are part of the binary-compatibility needs (symbols cannot disappear). And readding them spoils those solutions sadly for the goals, by what I see.