找不到在未命名命名空间中声明的 >>() 和 <<()

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 的命名空间,则上述更改不会发生任何变化。