下面的简化示例说明了流运算符>>和<<的问题。该示例在GCC10和GCC11中使用C++17标准编译,但在GCC 12.1中没有编译。在GCC 12中,未找到在未命名名称空间中声明的>>和<< ns_f::A运算符。
这是我的第一个问题,为什么运算符>>和<<不在GCC12中?GCC 12发生了什么变化,为什么改变了?
一个可能的解决方案是将操作符声明放入内联朋友函数中的A类中。但我喜欢将操作符隐藏在file.o模块中的版本。
在我的实验中,我发现在未命名的命名空间之后,类Wrapper的移动声明解决了这个问题。这是我的第二个问题,为什么?
#ifndef FILE_H
#define FILE_H
#include <istream>
#include <ostream>
#include <vector>
namespace ns_f {
struct A { };
class Z {
std::vector<A> items;
public:
void save_items(std::ostream& out) const;
};
} // namespace ns_f
#endif#include "file.h"
#include <fstream>
#include <istream>
template<typename T>
class Wrapper
{
public:
explicit Wrapper(T&) {}
private:
using P = typename T::value_type;
friend std::ostream& operator<<(std::ostream& out, const Wrapper&)
{
return out << P{};
}
};
namespace {
std::ostream& operator<<(std::ostream& out, const ns_f::A&)
{
return out;
}
} // unammed namespace
// If move declaration of Wrapper here it compiles.
namespace ns_f {
void Z::save_items(std::ostream& out) const
{
out << Wrapper(items);
}
} // namespace ns_f发布于 2022-05-26 17:49:08
当使用像<<这样的运算符(或者调用带有非限定名的函数)时,有两种方法可以找到匹配的名称(这里是operator<<重载)作为候选。
第一种方法是简单的非限定名查找,它从内到外遍历作用域,直到找到名称,然后停止。
第二种方法是通过依赖于参数的查找(ADL),在类的名称空间中查找它,这些类的类型显示为函数调用的参数的一部分,或者这里是<<操作符的操作数。
通常,这两种查找都是从调用出现的点开始执行的,并且不考虑只在该点之后在翻译单元中引入的声明。
但是,如果调用出现在模板中,就像out << P{};中的情况一样,可以从两个(甚至更多)点执行查找:定义包含调用的模板的点和实例化特定模板专门化的点。
在您的代码中,模板的定义在未命名命名空间中的operator<<重载声明之前,实例化在它之后(可能的点就在save_items定义之前或翻译单元的末尾)。
因此,一种可能的情况是,名称查找要么只从两个点中的某个点进行,要么从两个点中的一个点进行相同的查找。但实际的规则是,简单的非限定名称查找仅从定义点执行,而依赖于参数的查找则从实例化的角度执行。
因此只能通过ADL找到重载,但是ADL要求函数与参数的类型(这里不是这样)位于相同的名称空间中,因为A不是未命名名称空间的一部分。
这些规则是这样选择的,传统的理解是将特定于给定类的运算符重载放在与类本身相同的名称空间和标头中。
因此GCC 12是正确的,而以前的版本接受代码是错误的。
在版本12之前,GCC有一个bug,它还考虑了从实例化点查找不限定的名称,而不是在查找操作符重载以供操作符使用时的定义点。见bug 51577。
顺便说一下,未命名的名称空间是一条红鲱鱼。如果将operator<<重载直接放入全局命名空间或其他不是ns_f的名称空间中,则不会涉及上述更改。
https://stackoverflow.com/questions/72381728
复制相似问题