Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I wanted to write a generic << for any range and I ended up with this:

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 << ']';
}

Tested like so:

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

it works perfectly and outputs:

[1,2,3,4]

But, when tested with:

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

it outputs, unexpectedly:

[[,], ]

Running this code with a debugger, I came to a conclusion that the problem with empty range is that we run the return out << "[]";. Some C++ magic decided that my, just written,

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

is a better match then the, provided in <ostream>,

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

so instead of just sending "[]" to the output stream like we are used to see, it recurses back to itself, but with "[]" as the range argument.

What is the reason for that being a better match? Can I fix this in a more elegant manner compared to sending [ and ] separately?


EDIT: It appears that this is most likely a bug in GCC 10.1.0, since the newer versions reject the code.

like image 933
Fureeish Avatar asked Aug 22 '20 21:08

Fureeish


People also ask

Why do we use const char?

If you don't have the choice, using const char* gives a guarantee to the user that you won't change his data especially if it was a string literal where modifying one is undefined behavior. Show activity on this post. By using const you're promising your user that you won't change the string being passed in.

Why do we use const char in C?

In C programming language, *p represents the value stored in a pointer and p represents the address of the value, is referred as a pointer. const char* and char const* says that the pointer can point to a constant char and value of char pointed by this pointer cannot be changed.

What is the difference between const char * and char * const?

The difference is that const char * is a pointer to a const char , while char * const is a constant pointer to a char . The first, the value being pointed to can't be changed but the pointer can be. The second, the value being pointed at can change but the pointer can't (similar to a reference).

Can a char * be passed as const * argument?

In general, you can pass a char * into something that expects a const char * without an explicit cast because that's a safe thing to do (give something modifiable to something that doesn't intend to modify it), but you can't pass a const char * into something expecting a char * (without an explicit cast) because that's ...


1 Answers

I think this shouldn't compile. Let's simplify the example a bit to:

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{}, "");
}

The basic_thing/concrete_thing mimics what's going on with basic_ostream/ostream. #1 is the overload you're providing, #2 is the one in the standard library.

Clearly both of these overloads are viable for the call we're making. Which one is better?

Well, they're both exact matches in both arguments (yes, char const* is an exact match for "" even though we're undergoing pointer decay, see Why does pointer decay take priority over a deduced template?). So the conversion sequences can't differentiate.

Both of these are function templates, so can't differentiate there.

Neither function template is more specialized than the other - deduction fails in both directions (char const* can't match C auto&& and concrete_thing can't match basic_thing<T>).

The "more constrained" part only applies if the template parameter setup is the same in both cases, which is not true here, so that part is irrelevant.

And... that's it basically, we're out of tiebreakers. The fact that gcc 10.1 accepted this program was a bug, gcc 10.2 no longer does. Although clang does right now, and I believe that's a clang bug. MSVC rejects as ambiguous: Demo.


Either way, there's an easy fix here which is to write [ and then ] as separate characters.

And either way, you probably don't want to write

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

to begin with, since for that to actually work correctly you'd have to stick it in namespace std. Instead, you want to write a wrapper for an arbitrary range and use that instead:

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

And define your operator<< to print a print_view. That way, this just works and you don't have to deal with these issues. Demo.

Of course, instead of out << *current; you may want to conditionally wrap that in out << print_view{*current}; to be totally correct, but I'll leave that as an exercise.

like image 164
Barry Avatar answered Oct 09 '22 19:10

Barry