为什么 const char[] 与 std::ranges::range 的匹配比显式的 const char* 自由重载更好,以及如何修复它?

Fur*_*ish 6 c++ overload-resolution c++-concepts c++20 std-ranges

我想<<为任何人写一个泛型,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 << ']';
}
Run Code Online (Sandbox Code Playgroud)

像这样测试:

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

它完美运行并输出:

[1,2,3,4]
Run Code Online (Sandbox Code Playgroud)

但是,当测试时:

int main() {
    std::vector<int> empty = {};
    std::cout << empty << '\n';
}
Run Code Online (Sandbox Code Playgroud)

它出乎意料地输出:

[[,], ]
Run Code Online (Sandbox Code Playgroud)

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

std::ostream& operator << (std::ostream& out, std::ranges::range auto&& range);
Run Code Online (Sandbox Code Playgroud)

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

template< class Traits >
basic_ostream<char,Traits>& operator<<( basic_ostream<char,Traits>& os,  
                                        const char* s );
Run Code Online (Sandbox Code Playgroud)

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

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


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

Bar*_*rry 7

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

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{}, "");
}
Run Code Online (Sandbox Code Playgroud)

basic_thing/concrete_thing模仿是怎么回事什么basic_ostream/ ostream#1是您提供的重载,#2是标准库中的重载。

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

那么,他们在两个参数(是的,无论是精确匹配char const*是完全匹配"",即使系统正在进行指针腐烂,看为什么指针衰减优先于推导的模板吗?)。所以转换序列无法区分。

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

两个函数模板都比另一个更专业 - 两个方向的推导都失败(char const*can't matchC auto&&concrete_thingcan't match basic_thing<T>)。

“更受约束”部分仅适用于两种情况下模板参数设置相同的情况,这在此处不正确,因此该部分无关紧要。

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


无论哪种方式,这里都有一个简单的解决方法,即写入[然后]作为单独的字符。

不管怎样,你可能不想写

std::ostream& operator << (std::ostream& out, std::ranges::range auto&& range);
Run Code Online (Sandbox Code Playgroud)

首先,因为要使其真正正常工作,您必须将其粘贴在 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>>;
Run Code Online (Sandbox Code Playgroud)

并定义您operator<<要打印的print_view. 这样,这只是有效,您不必处理这些问题。演示

当然,out << *current;您可能希望有条件地将其包裹起来out << print_view{*current};以使其完全正确,但我会将其留作练习。