3

在编写与宇宙飞船运算符相关的 C++20 时,我注意到一些相当奇怪的东西。

据我了解,从 C++20 开始,比较运算符是由编译器自动生成的。但是,我遇到了一个关于自动生成运算符的有趣问题。

在下面的代码中,我试图定义MyIterator哪个派生自vector<int>::iterator. 现在我希望基类受到保护并显式地公开函数。所以很自然地,我使用using声明来使用基类中的成员函数。但是,编译器抱怨operator!=缺少!

发生这种情况是因为自动生成的运算符“生成得太晚”吗?

有趣的是,定义中显示的解决方法MyIterator2似乎可以解决问题。

我想听听这种奇怪行为的原因。这是通过 spaceship 运算符自动生成的运算符的预期行为吗?或者这是编译器错误还是未实现的功能?

编译器版本信息:

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

代码(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);
  }
};

补充笔记:

GCC 也以同样的方式拒绝代码。

[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!=;
      | 
4

3 回答 3

3

据我了解,从 C++20 开始,比较运算符是由编译器自动生成的。但是,我遇到了一个关于自动生成运算符的有趣问题。

这是不正确的。生成比较运算符。比较表达式被重写。

也就是说,给定:

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

X{2} == X{3}是直接有效的,调用 defaulted operator==,它进行成员比较,从而产生false.

X{2} != X{3}也是一个有效的表达式,但它不调用任何名为operator!=. 没有这样的功能。相反,它评估为!(X{2} == X{3}),产生true。尽管X{2} != X{3}是一个有效的表达式,但operator!=这里的任何地方都没有任何名称,因此您不能引用任何具有该名称的内容。

所以很自然,我使用 using 声明来使用基类中的成员函数。但是,编译器抱怨operator!=缺少!

在 C++20 中,我们几乎从不需要operator!=.,因此几乎所有这些都从标准库规范中删除,标准库很可能会通过并#ifdef删除它们。需要解析的代码更少。由于运算符重写,不等式表达式仍然有效,但operator!=不再有任何名称。

所以你不能using

但是标准库不再需要的同样原因operator!=也适用于你——你也不需要它。


进一步注意,即使在 C++17 中也不能保证using Base::operator!=;有效,因为没有义务operator!=将其编写为成员函数。它本来可以写成一个自由函数,然后这无论如何都不会起作用,即使在operator!=其他地方会有一个名为 ... 的函数。

于 2020-10-30T13:11:20.903 回答
1

的比较运算符std::vector<>::iterator可能是(实现定义的)并且最有可能实现为非成员函数

所以很自然,我使用 using 声明来使用基类中的成员函数。[...]

没有要求将其比较运算符实现为成员函数的iterator类型;std::vector根据[vector.overview]/3

类型iteratorconst_­iterator满足 constexpr 迭代器要求 ([iterator.requirements.general])。

[...]

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

恰恰相反,我们可能注意到所有迭代器适配器,由[predef.iterators]管理,将它们的比较运算符指定为非成员函数。

因此,GCC 和 Clang 拒绝您的程序都是正确的,并且还提供了准确的错误消息来说明他们拒绝它的原因。与以下简化示例进行比较:

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.
};
于 2020-10-30T08:59:36.983 回答
1

迭代器,包括 std::vector 迭代器,不必是类。

特别是 std 向量的迭代器是原始指针是完全合法的。

从指针继承不会很好地结束。

所以使用你的“继承和转发”技术是不安全的。正如您所了解的,它在标准库版本上也不是稳定的;该表达式it1 != it2保证可以工作,但如果库选择,它可能使用非成员!=,即使向量迭代器指向指针。


我可能建议的一个技巧是编写一个“索引器”。索引器接受一个类型T并对其进行迭代。我将它用于编写迭代器迭代器和编写整数迭代器。

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
};

等添加一些迭代器特征转发支持(通过部分专业化iterator_traits<indexing_iterator<T, true>>:iterator_traits<T>

现在,indexing_iterator<It>可以通过using声明安全地继承。你必须operator*自己写。

你可以编写for(auto it : iterators_of(Container))for ( auto i : indexes_of(Container))循环。

赢赢。

于 2020-10-30T20:56:01.987 回答