For Better or for Worse, the Overload
You know what’s stuck on my mind? Ever since writing my last post, it’s been the word “better.” It came up when we were talking about overload resolution and implicit conversion sequences. I explained a necessary special case of it—something about how adding const
in a reference-binding is preferred against—and then strategically shut up about the rest.
void run(int (**f)()); // #1
void run(int (*const *f)() noexcept); // #2
int foo() noexcept;
int (*p)() noexcept = &foo;
run(&p); // ???
But it’s so tantalizing, isn’t it? Which one will it choose? How can we reason about this? I can see it in your eyes, sore but eager. You yearn for conversion. Well, I wasn’t going to— I— well… Alright, since you’re so insistent. Just for you. Shall we?
∗ ∗ ∗
Let’s start small and work our way up. An implicit conversion sequence is a standard conversion sequence, possibly followed by a user-defined conversion and another standard conversion sequence in the case of a class type.1 A user-defined conversion is something like T::operator S()
, which defines how to convert a T
into an S
. These are easy: they work exactly how we tell them to. So, it evidently suffices to understand standard conversion sequences.
- Definition 1
-
A standard conversion sequence is a sequence of zero or one conversions from each of the following categories, in order:
- Lvalue-to-rvalue, array-to-pointer, or function-to-pointer conversions:
- Lvalue-to-rvalue: converts a glvalue of non-function, non-array type to a prvalue. Not particularly relevant to overload resolution, and kind of sophisticated, so we’ll mostly forget about this.
- Array-to-pointer: converts an expression of type “array of
T
” or “array of unknown bound ofT
” to a prvalue of type “pointer toT
,” applying temporary materialization conversion if the expression was a prvalue (note that GCC has a bug and won’t do this; temporary materialization is defined later). - Function-to-pointer: converts an lvalue function of type
T
to a prvalue of type “pointer toT
.”
- Integral/floating-point/boolean/pointer/pointer-to-member conversions and promotions:
- There are a bunch of rules for converting between various integral and floating-point types that are necessary but, frankly, menial and uninteresting, so we’ll omit these too. The pointer/pointer-to-member conversions are probably things you already know.
- Function pointer conversion: converts a prvalue of type “pointer to
noexcept
function” to a prvalue of type “pointer to function.” - Qualification conversion: unifies
const
ness of two types somehow. Oh boy. It can’t be that bad, right? Right?
- Lvalue-to-rvalue, array-to-pointer, or function-to-pointer conversions:
Surprise! This post is actually about qualification conversions
OK— OK. Uh. Hear me out.
In C++, const
and volatile
are often called cv-qualifiers, so called because they qualify types to form cv-qualified types. The cv-qualified versions of a cv-unqualified type T
are const T
, volatile T
, and const volatile T
. We could also consider types T
which have cv-qualifiers nested inside—for example, const int** const
(“const
pointer to pointer to const int
”) could be written alternatively as X
in the following series of type aliases:
using U = const int;
using V = U*;
using W = V*;
using X = const W;
Now, a mathematically inclined reader may choose to write “const
pointer to pointer to const int
” as
where , , , , and . More generally, we could write any type (not necessarily uniquely) as
for some and some type ; each is either “pointer to,” “array of ,” or “array of unknown size of.” For simplicity, let’s assume each will always be “pointer to.”
Notice that, for determining whether one type can be qualification-converted into another type (e.g., trying to convert int*
to const int*
), we can always drop from consideration altogether—in particular, at the top level, we can always initialize a const T
from a T
and vice versa, and likewise we can always convert from one to the other. So, let’s forget about .
Since we don’t care as much about any of the or —these are the “non-const
-y” parts, and we’ll deal with them separately—let’s write this even more compactly as the -tuple . The longest possible such tuple is called cv-qualification signature of .
We’re almost there. I’m trying really hard to make the C++ standard more palatable here, so bear with me. Two types and are called similar if they have cv-decompositions of equal size such that each of their respective ’s are either (1) the same, or (2) one is “array of ” and the other is “array of unknown size of”; and, moreover, their ’s should agree. Basically, if the “not-const
-y” parts of their cv-decompositions mostly agree, they’re called “similar.”
OK. It’s time. I’m only barely paraphrasing the standard because it’s all I can do at this point—it’s honestly worded pretty tightly. Let and be two types. Then, their cv-combined type , if it exists, is a type similar to such that, for each :