首页 > 解决方案 > 为什么 const char[] 更适合 std::ranges::range 而不是显式的 const char* 自由重载,以及如何解决它?

问题描述

我想<<为任何人写一个通用的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);

更好的匹配,然后提供<ostream>

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

因此,它不像我们通常看到的那样仅发送"[]"到输出流,而是递归回自身,但"[]"作为range参数。

那是更好的匹配的原因是什么?[]单独发送相比,我可以以更优雅的方式解决此问题吗?


编辑:这似乎很可能是 GCC 10.1.0 中的一个错误,因为较新的版本拒绝该代码。

标签: c++c++20overload-resolutionc++-conceptsstd-ranges

解决方案


我认为这不应该编译。让我们稍微简化一下这个例子:

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

首先,因为要真正正常工作,您必须将其粘贴在 namespace 中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. 这样,它就可以工作,而您不必处理这些问题。演示

当然,out << *current;您可能希望有条件地将其包装out << print_view{*current};为完全正确的,但我将把它作为练习。


推荐阅读