62
template <typename CRTP>
struct Pre {
    CRTP & operator++();
};

template <typename CRTP>
struct Post {
    CRTP operator++(int);
};

struct Derived
    : Pre<Derived>
    , Post<Derived>
{};

int main() {
    Derived d;
    d++;
    ++d;
}

I get these errors from GCC:

<source>: In function 'int main()':
<source>:18:10: error: request for member 'operator++' is ambiguous
        d++;
        ^~
<source>:8:14: note: candidates are: CRTP Post<CRTP>::operator++(int) [with CRTP = Derived]
        CRTP operator++(int);
            ^~~~~~~~
<source>:3:16: note:                 CRTP& Pre<CRTP>::operator++() [with CRTP = Derived]
        CRTP & operator++();
                ^~~~~~~~
<source>:19:11: error: request for member 'operator++' is ambiguous
        ++d;
        ^
<source>:8:14: note: candidates are: CRTP Post<CRTP>::operator++(int) [with CRTP = Derived]
        CRTP operator++(int);
            ^~~~~~~~
<source>:3:16: note:                 CRTP& Pre<CRTP>::operator++() [with CRTP = Derived]
        CRTP & operator++();
                ^~~~~~~~

Pre-decrement and post-decrement operators cause similar errors. No such errors with Clang. Any ideas what could be wrong or how to work around this?

jotik
  • 17,044
  • 13
  • 58
  • 123
  • 6
    `using Pre::operator++; using Post::operator++;` works, but I guess it defeats the purpose of your CRTP... – Quentin Jan 08 '19 at 12:11
  • 1
    fwiw also with supplying the implementation and also without crtp [gcc reports the error](https://wandbox.org/permlink/AqiMzKiLqrzZ7JCq) – 463035818_is_not_an_ai Jan 08 '19 at 12:14
  • 3
    @Quentin Puts using declaration in a helper template `PrePost : Pre, Post` – felix Jan 08 '19 at 12:16
  • 8
    For me behavior of gcc seems to be correct. Invocation of function `operator ++` should not compile because it is not clear to which function does the name `operator ++` refer to. – user7860670 Jan 08 '19 at 12:17
  • It compiles with clang 6 and up as well as with Visual Studio 2017. – Jabberwocky Jan 08 '19 at 12:29
  • As a [highly upvoted answer](https://stackoverflow.com/a/54091645/545127) to this question indicates, it is questionable whetehr this is a compiler bug rather than a fault in your program, which the compiler is correctly reporting. – Raedwald Jan 08 '19 at 13:00
  • @Raedwald [That answer](https://stackoverflow.com/a/54091645/3919155) states "GCC is correct to report it", hence one could conclude that it is Clang which incorrectly doesn't report an error. Only a comment by Walter to that answer hints that this might be a language defect. I detected the comment only after editing my tags, but now reverted it to your edit. – jotik Jan 08 '19 at 13:03
  • 2
    It's not a defect in the sense that the language itself has an inconsistency that needs resolution. It's only a design choice with unfortunate consequences, a colloquial defect, if you were. – StoryTeller - Unslander Monica Jan 08 '19 at 13:07
  • @StoryTeller So it is actually Clang and Visual Studio which incorrectly accept the code as valid? – jotik Jan 08 '19 at 13:52
  • @jotik - Formally yes, they are wrong. Even though I agree with the sentiment that accepting it would feel more natural. – StoryTeller - Unslander Monica Jan 08 '19 at 13:54

1 Answers1

64

Name lookup must occur first. In this case for the name operator++.

[basic.lookup] (emphasis mine)

1 The name lookup rules apply uniformly to all names (including typedef-names ([dcl.typedef]), namespace-names ([basic.namespace]), and class-names ([class.name])) wherever the grammar allows such names in the context discussed by a particular rule. Name lookup associates the use of a name with a declaration ([basic.def]) of that name. Name lookup shall find an unambiguous declaration for the name (see [class.member.lookup]). Name lookup may associate more than one declaration with a name if it finds the name to be a function name; the declarations are said to form a set of overloaded functions ([over.load]). Overload resolution ([over.match]) takes place after name lookup has succeeded. The access rules (Clause [class.access]) are considered only once name lookup and function overload resolution (if applicable) have succeeded. Only after name lookup, function overload resolution (if applicable) and access checking have succeeded are the attributes introduced by the name's declaration used further in expression processing (Clause [expr]).

And only if the lookup is unambiguous, will overload resolution proceed. In this case, the name is found in the scope of two different classes, and so an ambiguity is present even prior to overload resolution.

[class.member.lookup]

8 If the name of an overloaded function is unambiguously found, overloading resolution ([over.match]) also takes place before access control. Ambiguities can often be resolved by qualifying a name with its class name. [ Example:

struct A {
  int f();
};

struct B {
  int f();
};

struct C : A, B {
  int f() { return A::f() + B::f(); }
};

— end example ]

The example pretty much summarizes the rather long lookup rules in the previous paragraphs of [class.member.lookup]. There is an ambiguity in your code. GCC is correct to report it.


As for working around this, people in comments already presented the ideas for a workaround. Add a helper CRTP class

template <class CRTP>
struct PrePost
    : Pre<CRTP>
    , Post<CRTP>
{
    using Pre<CRTP>::operator++;
    using Post<CRTP>::operator++;
};

struct Derived : PrePost<Derived> {};

The name is now found in the scope of a single class, and names both overloads. Lookup is successful, and overload resolution may proceed.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 12
    Fwiw, MS VS2015 (19.00.24215.1) complains about the ambiguity via IntelliNonsense, but then still compiles the code to success (with no warnings or errors) and executes the hoped-for members at run-time. – WhozCraig Jan 08 '19 at 12:24
  • 5
    @StoryTeller Indeed, for a "normal" method `f()` vs. `f(int)`, `clang++` complains about: "error: member 'f' found in multiple base classes of different types" and adds "note: member found by ambiguous name lookup". GCC is actually more consistent here. – Arne Vogel Jan 08 '19 at 12:30
  • 4
    Ok. So it appears that this is sort-of a defect in the language (that `d++` and `++d` refer to the same operator/function name), which gcc faithfully implements, while clang and VS17 appear to implement what the user obviously intended (even w/o warning). – Walter Jan 08 '19 at 12:33
  • 2
    @Walter - Pretty much. Had we taken a path like, say, Python with regards to overloading, it'd had probably been easier to work with all around. Alas, something else was choesn. – StoryTeller - Unslander Monica Jan 08 '19 at 12:41
  • @StoryTeller Well, that is progress -- after all python has been conceived after some experience with C++. – Walter Jan 08 '19 at 14:36
  • 1
    @Walter - Python does not have pre/post (in|de)crement operators – Happy Green Kid Naps Jan 08 '19 at 16:19
  • Yeah, that ambiguity is unfortunate. Do you have any alternative design in mind? – Rakete1111 Jan 08 '19 at 16:38
  • 1
    @Rakete1111 - Pick names for those functions from the heaps of reserved identifiers the standard keeps? I did mention Python for a reason. It also allows for extension more easily than `operator `. – StoryTeller - Unslander Monica Jan 08 '19 at 16:41
  • IMHO clang is right, not gcc. What you described is correct for set of overloaded functions like foo(float)/foo(int) or operator+(int)/operator+(float) - but operator++() and operator++(int) should be treated as functions of different names. This is only kind of, as already said, coincidence that these two different operators looks like sharing the same name. – PiotrNycz Jan 08 '19 at 19:51
  • 2
    @PiotrNycz - Other than being called by a special syntax, overloaded operators **are** regular functions. As a matter of fact, they can also be called just like regular functions. `d.operator++(0)` will cause this ambiguity, as should `d++`, since the two are exactly equivalent. – StoryTeller - Unslander Monica Jan 08 '19 at 20:24
  • It ought to be possible for a compiler to "optimise" `d++` to `++d` where the return value is discarded (after all, that's how it works for built-in types, no?). [note to self: must actually check what standard says ...] – Will Crawford Jan 09 '19 at 04:13
  • @WillCrawford: The compiler can "optimize" `d++` to the equivalent of `++d` under the [as-if rule](https://en.cppreference.com/w/cpp/language/as_if), if and only if it can prove that no observable side effects are lost (so, it must be able to see the bodies of both functions and prove that they do the same thing). For the related idea of "optimizing" `return ++d;` into `++d; return d;` see [P1155 "More implicit moves"](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1155r1.html#further). (Revision 2 will add `++` and `--` to the list of proposed operators.) – Quuxplusone Jan 09 '19 at 04:17
  • @Quuxplusone thank you indeed -- the standard is both long and dry as reading material. And I wouldn't have seen that proposal either. – Will Crawford Jan 09 '19 at 05:03
  • 1
    +1. This is not limited to pre/postfix `++/--`; binary and unary `+/-/*/&` are considered to have the same name as well. – T.C. Jan 09 '19 at 10:35