C++17’s {} impeding SC for new method overloads

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:

  1. to enable API consumers to write code which works as if there are the two old and the one new overloads declared
  2. 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.

16 thoughts on “C++17’s {} impeding SC for new method overloads

    • 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.

  1. 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.

    • 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.

    • 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.