C++20: Automatically generated operators are unreferencable in derived classes?

Viewed 223

I noticed something rather strange while writing C++20 related to spaceship operators.

As I understand, since C++20, comparison operators are automatically generated by the compiler. However, I have encountered an interesting problem with this automatic generation of operators.

In the code below, I am trying to define MyIterator which is derived from vector<int>::iterator. Now I want the base class to be protected and expose functions explicitly. So naturally, I use using declarations to use member functions from the base class. However, the compiler complains that operator!= is missing!

Does this happen because the automatically generated operators are "generated too late"?

Interestingly, the workaround shown in the definition of MyIterator2 seems to fix the problem.

I would like to hear the cause of this weird behavior. Is this the intended behavior for automatically generated operators via the spaceship operator? Or is this a compiler bug or an unimplemented feature?

Compiler version information:

[hoge@foobar]$ clang++ --version
clang version 10.0.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

Code (main.cpp):

#include <vector>

using MyVec = std::vector<int>;

/// Does not compile
class MyIterator : protected MyVec::iterator {
  using Base = MyVec::iterator;

  // Error
  // No member named 'operator!=' in '__gnu_cxx::__normal_iterator<int *,
  // std::vector<int, std::allocator<int>>>'clang(no_member)
  using Base::operator!=;
};

/// Compiles
class MyIterator2 : protected MyVec::iterator {
  using Base = MyVec::iterator;

  /// A rather ugly workaround!
  auto operator!=(const MyIterator2 &o) const {
    return static_cast<const Base &>(*this) != static_cast<const Base &>(o);
  }
};

Additional notes:

GCC also rejects the code in the same way.

[hoge@foobar tmp]$ g++ --version
g++ (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
[hoge@foobar tmp]$ g++ main.cpp
main.cpp:12:23: error: ‘operator!=’ has not been declared in ‘__gnu_cxx::Base’
   12 |   using Base::operator!=;
      | 
3 Answers

As I understand, since C++20, comparison operators are automatically generated by the compiler. However, I have encountered an interesting problem with this automatic generation of operators.

This is not correct. The comparison operators are not generated. Comparison expressions are rewritten.

That is, given:

struct X {
    int i;
    bool operator==(X const&) const = default;
};

X{2} == X{3} is straightforwardly valid, invokes the defaulted operator==, which does member-wise comparison, and thus yields false.

X{2} != X{3} is also a valid expression, but it does not invoke anything named operator!=. There is no such function. Instead, it evaluates as !(X{2} == X{3}), yielding true. Despite X{2} != X{3} being a valid expression, there is nothing named operator!= anywhere here, so you cannot reference anything with that name.

So naturally, I use using declarations to use member functions from the base class. However, the compiler complains that operator!= is missing!

In C++20, we almost never actually need an operator!=, so nearly all of them were removed from the standard library specification, and the standard libraries likely went through and #ifdef-ed them out. Less code to have to parse. The inequality expressions still work, due to the operator rewrites, but there isn't anything named operator!= anymore.

So you can't using it.

But the same reason that the standard library doesn't need operator!= anymore also applies to you - you don't need it either.


Note further that there's no guarantee that using Base::operator!=; worked even in C++17, since there's no obligation for operator!= to have been written as a member function. It could have been written as a free function, and then this wouldn't have worked anyway, even though there would be a function named operator!=... just somewhere else.

The comparison operators of std::vector<>::iterator may be (implementation-defined) and is most likely implemented as non-member functions

So naturally, I use using declarations to use member functions from the base class. [...]

There is no requirement for the iterator type of std::vector to implement its comparison operators as member functions; as per [vector.overview]/3

The types iterator and const_­iterator meet the constexpr iterator requirements ([iterator.requirements.general]).

[...]

using iterator = implementation-defined; // see [container.requirements]

Rather the opposite, and we may note that all iterator adaptors, as governed by [predef.iterators], specifies their comparison operators as non-member functions.

Thus, both GCC and Clang are correct to reject your program, and also provides accurate error messages as for why they reject it. Compare with the following simplified example:

struct A {
    int x;
    // Non-member operator==.
    friend bool operator==(const A&, const A&) = default;
};

struct B {
    int x;
    // Member operator==.
    bool operator==(const B&) const = default;
};

struct ADerived : public A {
    using A::operator==;  // Error: no MEMBER named 'operator==' in 'A'
};

struct BDerived : public B {
    using B::operator==;  // OK.
};

Iterators, including std::vector iterators, do not have to be classes.

In particular it is perfectly legal for std vector's iterators to be raw pointers.

Inheriting from a pointer won't end well.

So using your "inherit and forward" technique is not safe. Nor, as you have learned, is it stable over std library versions; the expression it1 != it2 was guaranteed to work, but it could have used a non-member != if the library chose, even if vector iterators where pointers.


One trick I might suggest is to write an "indexer". An indexer takes a type T and iterates over it. I use it both for writing iterator-iterators and for writing integer-iterators.

template<class T, bool forward_iterator_traits=false>
struct indexing_iterator {
  T value;
  T const& operator*() const { return value; }
  indexing_iterator& operator++()
  {
    ++value;
    return *this;
  }
  indexing_iterator operator++(int)
  {
    indexing_iterator tmp(*this); // copy
    ++*this;
    return tmp;   // return old value
  }
  // etc
};

etc. Add in some iterator trait forwarding support (by partial specializing iterator_traits<indexing_iterator<T, true>>:iterator_traits<T>)

Now, indexing_iterator<It> can be inherited from safely with using declarations. You do have to write operator* yourself.

And you can write for(auto it : iterators_of(Container)) and for ( auto i : indexes_of(Container)) loops.

Win win.

Related