Why Does Pointer Decay Take Priority Over a Deduced Template

Why does pointer decay take priority over a deduced template?

The fundamental reason for this (standard-conforming) ambiguity appears to lie within the cost of conversion: Overload resolution tries to minimize the operations performed to convert an argument to the corresponding parameter. An array is effectively the pointer to its first element though, decorated with some compile-time type information. An array-to-pointer conversion doesn't cost more than e.g. saving the address of the array itself, or initializing a reference to it. From that perspective, the ambiguity seems justified, although conceptually it is unintuitive (and may be subpar). In fact, this argumentation applies to all Lvalue Transformations, as suggested by the quote below. Another example:

void g() {}

void f(void(*)()) {}
void f(void(&)()) {}

int main() {
f(g); // Ambiguous
}

The following is obligatory standardese. Functions that are not specializations of some function template are preferred over ones that are if both are otherwise an equally good match (see [over.match.best]/(1.3), (1.6)). In our case, the conversion performed is an array-to-pointer conversion, which is an Lvalue Transformation with Exact Match rank (according to table 12 in [over.ics.user]). [over.ics.rank]/3:

  • Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if

    • S1 is a proper subsequence of S2 (comparing the conversion sequences in the canonical form defined by 13.3.3.1.1, excluding
      any Lvalue Transformation
      ; the identity conversion sequence is considered to be a subsequence of any non-identity conversion
      sequence) or, if not that,

    • the rank of S1 is better than the rank of S2, or S1 and S2 have the same rank and are distinguishable by the rules in the paragraph below, or, if not that,

    • [..]

The first bullet point excludes our conversion (as it is an Lvalue Transformation). The second one requires a difference in ranks, which isn't present, as both conversions have Exact match rank; The "rules in the paragraph below", i.e. in [over.ics.rank]/4, don't cover array-to-pointer conversions either.

So believe it or not, none of both conversion sequences is better than the other, and thus the char const*-overload is picked.


Possible workaround: Define the second overload as a function template as well, then partial ordering kicks in and selects the first one.

template <typename T>
auto foo(T s)
-> std::enable_if_t<std::is_convertible<T, char const*>{}>
{
std::cout << "raw, size=" << std::strlen(s) << std::endl;
}

Demo.

array decay to pointer and overload resolution

You need to make the first overload a poorer choice when both are viable. Currently they are a tie on conversion ranking (both are "Exact Match"), and then the tie is broken because non-templates are preferred.

This ought to make the conversion ranking worse:

struct stg
{
struct cvt { const char* p; cvt(const char* p_p) : p(p_p) {} };

// matches const char*, but disfavored in overload ranking
stg(cvt c_str); // use c_str.p inside :( Or add an implicit conversion

template<int N>
stg(const char (&str) [N]);
};

C++ template for all pointers and template for all arrays

A C++98 solution is to have the pointer-taking operator take a const reference to a pointer.

#include <iostream>

struct Class
{
template<typename T>
void operator&(T* const &p)
{
std::cout << "general pointer operator " << (*p) << std::endl;
}

template<typename T, unsigned int N>
void operator&(T (&)[N])
{
std::cout << "general array operator " << N << std::endl;
}
};

int main()
{
int myarr[1] = { 2 };
int* p = myarr;
Class obj;

obj & myarr;
obj & p;

return 0;
}

Output:

general array operator 1
general pointer operator 2

Overload resolution and arrays: which function should be called?

First, the conversion sequence of all three is the same, except that for the first two, there is an lvalue transformation (lvalue to rvalue conversion), which however is not used in ordering conversion sequences. All three are exact matches (the function template specialization has parameter type char const(&)[2]).

If you iterate over the rules at 13.3.3.2p3, you stop at this paragraph

S1 and S2 are reference bindings (8.5.3) and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference.

A conversion sequence cannot be formed if it requires binding an rvalue reference to an lvalue, the spec says at 13.3.3.1.4p3. If you look at how reference binding works at 8.5.3p5 last bullet, it will create a temporary (I think they meant rvalue temporary) of type char const* from the array lvalue and bind the reference to that temporary. Therefor, I think (1) is better than (2). Same holds for (1) against (3), although we wouldn't need this because (3) is a template so in a tie, we would choose (1) again.

In n3225, they changed the reference binding rules so that rvalue references can bind to initializer expressions that are lvalues, as long as the reference will be bound to an rvalue (possibly created by converting the initializer properly before). This could influence the handling by Visual C++, which may not be up to date here.

I'm not sure about clang. Even if it would ignore (1), then it would end up in a tie between (2) and (3), and would need to choose (2) because it's a non-template.


I think that 8.5.3p5 last bullet is confusing because it says "Otherwise a temporary of type ..". It's not clear whether the temporary is regarded as an lvalue or as an rvalue by 13.3.3.1.4p3, which means I'm not sure how the following should really behave according to the exact words of the spec

void f(int &);
void f(int &&);

int main() {
int n = 0;
f(n);
}

If we assume the temporary is treated as an rvalue by clause 13, then we bind an rvalue ref to an rvalue in the second function and an lvalue in the first. Therefor, we will choose the second function and then get a diagnostic by 8.5.3p5 last bullet because T1 and T2 are reference-related. If we assume the temporary is treated as an lvalue by clause 13, then the following would not work

void f(int &&);
int main() {
f(0);
}

Because we would bind an rvalue ref to an lvalue which by clause 13 will make the function non-viable. And if we interpret "binding an rvalue ref to an lvalue" to refer to the initializer expression instead of the final expression bound to, we won't accept the following

void f(float &&);
int main() {
int n = 0;
f(n);
}

This however is valid as of n3225. So there seems to be some confusion - I sent a DR to the committee about this.

C++ function match priority

Overload resolution attempts to find the best conversion. The following paragraph lists the relevant bullet points that could distinguish both conversions:

Standard conversion sequence S1 is a better conversion sequence than
standard conversion sequence S2 if

  • S1 is a proper subsequence of S2 (comparing the conversion sequences in the canonical form defined by 13.3.3.1.1, excluding any
    Lvalue Transformation
    ; the identity conversion sequence is considered
    to be a subsequence of any non-identity conversion sequence) or, if
    not that,

  • the rank of S1 is better than the rank of S2, or S1 and S2 have the same rank and are distinguishable by the
    rules in the paragraph below, or, if not that,

  • […]

While the specialization of the function template yields a parameter with an identity conversion, the non-template overload with char const* requires an Array-to-pointer conversion. Intuitively, we'd say that the former is a better match and should thus be selected. However, the array-to-pointer conversion is an Lvalue Transformation, excluded from the first bullet point. And since it has Exact Match Rank, the rank of the conversion isn't different than the rank of the conversion for char const (&)[N], which also has Exact Match Rank. "The rules in the paragraph below" cannot distinguish the conversions either, as they solely address derived-to-base conversions and such, but not array-to-pointer.

In fact, the conversion to char const (&)[N] is not better in any way. But overload resolution discriminates templates:

Given these definitions, a viable function F1 is defined to be a
better function than another viable function F2 if for all arguments
i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then

  • for some argument j, ICSj(F1) is a better conversion
    sequence than ICSj(F2), or, if not that,

  • […]

  • F1
    is not a function template specialization and F2 is a function
    template specialization, or, if not that,

Hence the non-template overload is selected.

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



Related Topics



Leave a reply



Submit