为什么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,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*""的完全匹配,即使我们正在经历指针衰减,请参见Why does pointer decay take priority over a deduced template?)。因此转换顺序无法区分。

这两个都是函数模板,因此无法在其中区分。

这两个功能模板都不比另一个更专业-推导在两个方向上都失败(char const*C auto&&不匹配,而concrete_thingbasic_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 << *current;中而不是out << print_view{*current};,但我将保留它作为练习。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...