By jotik


2019-01-08 12:07:45 8 Comments

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?

1 comments

@StoryTeller 2019-01-08 12:16:34

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.

@WhozCraig 2019-01-08 12:24:47

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.

@Arne Vogel 2019-01-08 12:30:53

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

@Walter 2019-01-08 12:33:33

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

@StoryTeller 2019-01-08 12:41:03

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

@Walter 2019-01-08 14:36:43

@StoryTeller Well, that is progress -- after all python has been conceived after some experience with C++.

@Happy Green Kid Naps 2019-01-08 16:19:20

@Walter - Python does not have pre/post (in|de)crement operators

@Rakete1111 2019-01-08 16:38:37

Yeah, that ambiguity is unfortunate. Do you have any alternative design in mind?

@StoryTeller 2019-01-08 16:41:35

@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 <token>.

@PiotrNycz 2019-01-08 19:51:59

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.

@StoryTeller 2019-01-08 20:24:54

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

@Will Crawford 2019-01-09 04:13:18

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

@Quuxplusone 2019-01-09 04:17:49

@WillCrawford: The compiler can "optimize" d++ to the equivalent of ++d under the as-if rule, 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". (Revision 2 will add ++ and -- to the list of proposed operators.)

@Will Crawford 2019-01-09 05:03:42

@Quuxplusone thank you indeed -- the standard is both long and dry as reading material. And I wouldn't have seen that proposal either.

@T.C. 2019-01-09 10:35:01

+1. This is not limited to pre/postfix ++/--; binary and unary +/-/*/& are considered to have the same name as well.

Related Questions

Sponsored Content

27 Answered Questions

[SOLVED] Easiest way to convert int to string in C++

21 Answered Questions

[SOLVED] What is the "-->" operator in C++?

35 Answered Questions

16 Answered Questions

[SOLVED] What is the difference between const int*, const int * const, and int const *?

31 Answered Questions

10 Answered Questions

[SOLVED] What is the difference between g++ and gcc?

  • 2008-10-05 20:25:13
  • Brian R. Bondy
  • 386775 View
  • 746 Score
  • 10 Answer
  • Tags:   c++ gcc g++

1 Answered Questions

7 Answered Questions

1 Answered Questions

[SOLVED] C++ compiler can't chose overloading operator*

1 Answered Questions

Sponsored Content