为什么 const char[] 更适合 std::ranges::range 而不是显式的 const char* 自由重载,以及如何修复它?

Why is const char[] a better match for std::ranges::range than for an explicit, const char* free overload, and how to fix it?

我想为任何 range 写一个通用的 <<,最后我得到了这个:

std::ostream& operator << (std::ostream& out, std::ranges::range auto&& range) {
    using namespace std::ranges;

    if (empty(range)) {
        return out << "[]";
    }

    auto current = begin(range);
    out << '[' << *current;

    while(++current != end(range)) {
        out << ',' << *current;
    }

    return out << ']';
}

像这样测试:

int main() {
    std::vector<int> ints = {1, 2, 3, 4};
    std::cout << ints << '\n';
}

它完美运行并输出:

[1,2,3,4]

但是,测试时:

int main() {
    std::vector<int> empty = {};
    std::cout << empty << '\n';
}

它意外地输出:

[[,], ]

运行这段代码用调试器,我得出的结论是空范围的问题是我们运行 return out << "[]";。一些 C++ 魔法决定我的,刚刚写的,

std::ostream& operator << (std::ostream& out, std::ranges::range auto&& range);

更匹配 provided in <ostream>

template< class Traits >
basic_ostream<char,Traits>& operator<<( basic_ostream<char,Traits>& os,  
                                        const char* s );

所以它不像我们过去看到的那样只是将 "[]" 发送到输出流,而是递归回到自身,但是使用 "[]" 作为 range 参数。

匹配更好的原因是什么?与分别发送 [] 相比,我能否以更优雅的方式解决此问题?


编辑:看来这很可能是 GCC 10.1.0 中的错误,因为较新的版本 reject 代码。

我认为这个不应该编译。让我们将示例稍微简化为:

template <typename T> struct basic_thing { };
using concrete_thing = basic_thing<char>;

template <typename T> concept C = true;

void f(concrete_thing, C auto&&); // #1
template <typename T> void f(basic_thing<T>, char const*); // #2

int main() {
    f(concrete_thing{}, "");
}

basic_thing/concrete_thing 模仿了 basic_ostream/ostream 的情况。 #1 是您提供的重载,#2 是标准库中的重载。

很明显,这两个重载对于我们正在进行的调用都是可行的。哪个更好?

好吧,它们在两个参数中都完全匹配(是的,char const*"" 完全匹配,即使我们正在进行指针衰减,请参阅 ) .所以转换序列无法区分

这两个都是函数模板,无法区分。

两个函数模板都不比另一个更专业 - 推导在两个方向上都失败(char const* 无法匹配 C auto&& 并且 concrete_thing 无法匹配 basic_thing<T> ).

“更多约束”部分仅适用于两种情况下模板参数设置相同的情况,但此处并非如此,因此该部分无关紧要。

而且...基本上就是这样,我们没有决胜局。 gcc 10.1 接受这个程序的事实是一个错误,gcc 10.2 不再接受。虽然现在 clang 确实如此,但我相信这是一个 clang 错误。 MSVC 拒绝为不明确:Demo.


无论如何,这里有一个简单的解决方法,就是将 [] 分别写成单独的字符。

无论哪种方式,您可能都不想写

std::ostream& operator << (std::ostream& out, std::ranges::range auto&& range);

首先,因为要使其真正正常工作,您必须将其粘贴在命名空间 std 中。相反,您想为任意范围编写包装器并改用它:

template <input_range V> requires view<V>
struct print_view : view_interface<print_view<V>> {
    print_view() = default;
    print_view(V v) : v(v) { }

    auto begin() const { return std::ranges::begin(v); }
    auto end() const { return std::ranges::end(v); }

    V v;
};

template <range R>
print_view(R&& r) -> print_view<all_t<R>>;

并定义您的 operator<< 以打印 print_view。这样,这就可以正常工作,您不必处理这些问题。 Demo.

当然,您可能希望有条件地将其包装在 out << print_view{*current}; 中而不是 out << *current; 以完全正确,但我会把它留作练习。