找不到在未命名命名空间中声明的 >>() 和 <<()
Cannot find >>() and <<() declared in unnamed namespace
下面的简化示例说明了流运算符 >>
和 <<
的问题。该示例在符合 C++17 标准的 GCC10 和 GCC11 中编译,但在 GCC 12.1 中不编译。在 GCC 12 中,找不到在未命名命名空间中声明的 ns_f::A
的运算符 >>
和 <<
。
这是我的第一个问题,为什么在 GCC12 中找不到运算符 >>
和 <<
? GCC 12 中发生了什么变化,为什么会发生变化?
一种可能的解决方案是将运算符声明放入内联友元函数的 class A 中。但我喜欢运算符隐藏在 file.o 模块中的版本。
在我的实验中,我发现在未命名的命名空间之后移动 class 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
当使用像 <<
这样的运算符(或调用具有非限定名称的函数)时,有两种方法可以找到匹配名称(即这里的 operator<<
重载)作为候选名称。
第一种方法是通过简单的非限定名称查找,从内到外遍历范围,直到找到名称,然后停止。
第二个是通过 argument-dependent 查找 (ADL),在 classes 的名称空间中查找它,其类型显示为函数调用参数的一部分,或此处的操作数<<
运算符。
通常这两种查找都是从调用出现的点开始执行的,并且不考虑在翻译单元中仅在该点之后引入的声明。
但是,如果调用出现在模板中,如 out << P{};
的情况,则有 两个 (或可能更多)点可以从中查找performed: 定义包含调用的模板的点和实例化特定模板特化的点。
在您的代码中,模板的定义在未命名命名空间中的 operator<<
重载声明之前,实例化在它之后(可能的点紧接在 save_items
或在翻译单元的末尾)。
因此,一种可能性是仅从两个点之一或同时从两个点进行名称查找。但实际规则是,简单的非限定名称查找 仅从定义的角度执行 ,而 argument-dependent 的查找从实例化的角度执行。
所以你的重载只能通过 ADL 找到,但 ADL 要求函数与参数的类型位于相同的命名空间中,但这里不是这种情况,因为 A
不是未命名命名空间的一部分。
以这种方式选择规则的传统理解是,您会将特定于给定 class 的运算符重载放在与 class 本身相同的命名空间和 header 中。
因此 GCC 12 是正确的,以前的版本接受代码是错误的。
在版本 12 之前,GCC 有一个错误,在查找运算符重载以供运算符使用时,该错误还考虑了从实例化点而不是定义点进行的非限定名称查找。参见 bug 51577。
顺便说一句,未命名的命名空间是一个转移注意力的问题。如果您将 operator<<
重载直接放入全局命名空间或任何其他非 ns_f
的命名空间,则上述更改不会发生任何变化。
下面的简化示例说明了流运算符 >>
和 <<
的问题。该示例在符合 C++17 标准的 GCC10 和 GCC11 中编译,但在 GCC 12.1 中不编译。在 GCC 12 中,找不到在未命名命名空间中声明的 ns_f::A
的运算符 >>
和 <<
。
这是我的第一个问题,为什么在 GCC12 中找不到运算符 >>
和 <<
? GCC 12 中发生了什么变化,为什么会发生变化?
一种可能的解决方案是将运算符声明放入内联友元函数的 class A 中。但我喜欢运算符隐藏在 file.o 模块中的版本。
在我的实验中,我发现在未命名的命名空间之后移动 class 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
当使用像 <<
这样的运算符(或调用具有非限定名称的函数)时,有两种方法可以找到匹配名称(即这里的 operator<<
重载)作为候选名称。
第一种方法是通过简单的非限定名称查找,从内到外遍历范围,直到找到名称,然后停止。
第二个是通过 argument-dependent 查找 (ADL),在 classes 的名称空间中查找它,其类型显示为函数调用参数的一部分,或此处的操作数<<
运算符。
通常这两种查找都是从调用出现的点开始执行的,并且不考虑在翻译单元中仅在该点之后引入的声明。
但是,如果调用出现在模板中,如 out << P{};
的情况,则有 两个 (或可能更多)点可以从中查找performed: 定义包含调用的模板的点和实例化特定模板特化的点。
在您的代码中,模板的定义在未命名命名空间中的 operator<<
重载声明之前,实例化在它之后(可能的点紧接在 save_items
或在翻译单元的末尾)。
因此,一种可能性是仅从两个点之一或同时从两个点进行名称查找。但实际规则是,简单的非限定名称查找 仅从定义的角度执行 ,而 argument-dependent 的查找从实例化的角度执行。
所以你的重载只能通过 ADL 找到,但 ADL 要求函数与参数的类型位于相同的命名空间中,但这里不是这种情况,因为 A
不是未命名命名空间的一部分。
以这种方式选择规则的传统理解是,您会将特定于给定 class 的运算符重载放在与 class 本身相同的命名空间和 header 中。
因此 GCC 12 是正确的,以前的版本接受代码是错误的。
在版本 12 之前,GCC 有一个错误,在查找运算符重载以供运算符使用时,该错误还考虑了从实例化点而不是定义点进行的非限定名称查找。参见 bug 51577。
顺便说一句,未命名的命名空间是一个转移注意力的问题。如果您将 operator<<
重载直接放入全局命名空间或任何其他非 ns_f
的命名空间,则上述更改不会发生任何变化。