const char []が明示的なconstchar *フリーオーバーロードよりもstd :: ranges :: rangeに適しているのはなぜですか、またそれを修正する方法は?

Aug 23 2020

私は<<任意のジェネリックを書きたかったのですが、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);

より良い試合は、その後で提供します

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

したがって"[]"、これまでのように出力ストリームに送信するのではなく"[]"range引数として、それ自体に再帰的に戻ります。

それがより良い一致である理由は何ですか?私は、送信に比べて、よりエレガントな方法でこの問題を解決することができ[かつ]個別に?


編集:新しいバージョンはコードを拒否するため、これはGCC10.1.0のバグである可能性が高いようです。

回答

6 Barry Aug 23 2020 at 04:40

これコンパイルすべきではないと思います。例を少し単純化して、次のようにします。

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がこのプログラムを受け入れたという事実はバグでしたが、gcc10.2はもはや受け入れません。clangは今は機能しますが、それはclangのバグだと思います。MSVCはあいまいなものとして拒否します:デモ。


いずれにせよ、ここに簡単な修正があります。それは、書き込み[を行ってから]、別々の文字として書き込むことです。

そしてどちらにしても、あなたはおそらく書きたくないでしょう

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。そうすれば、これはうまく機能し、これらの問題に対処する必要はありません。デモ。

もちろん、完全に正しいようout << *current;に条件付きでラップするのでout << print_view{*current};はなく、それを演習として残しておきます。