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:

  1. 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 NN T” or “array of unknown bound of T” to a prvalue of type “pointer to T,” 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 to T.”
  2. 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.
  3. Function pointer conversion: converts a prvalue of type “pointer to noexcept function” to a prvalue of type “pointer to function.”
  4. Qualification conversion: unifies constness of two types somehow. Oh boy. It can’t be that bad, right? Right?

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

cv0 P0 cv1 P1 cv2 Ucv_0~P_0~cv_1~P_1~cv_2~\mathtt{U}

where cv0={const}cv_0=\{\mathtt{const}\}, cv1=cv_1=\emptyset, cv2={const}cv_2=\{\mathtt{const}\}, P0=P1=“pointer to”P_0=P_1=\text{``pointer to''}, and U=int\mathtt{U}=\mathtt{int}. More generally, we could write any type T\mathtt{T} (not necessarily uniquely) as

cv0 P0 cv1 P1  cvn1 Pn1 cvn Ucv_0~P_0~cv_1~P_1~\ldots~cv_{n-1}~P_{n-1}~cv_n~\mathtt{U}

for some n0n\ge 0 and some type U\mathtt{U}; each PiP_i is either “pointer to,” “array of NiN_i,” or “array of unknown size of.” For simplicity, let’s assume each PiP_i 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 cv0cv_0 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 cv0cv_0.

Since we don’t care as much about any of the PiP_i or U\mathtt{U}—these are the “non-const-y” parts, and we’ll deal with them separately—let’s write this even more compactly as the nn-tuple (cv1,cv2,,cvn)(cv_1,cv_2,\ldots,cv_n). The longest possible such tuple is called cv-qualification signature of T\mathtt{T}.

We’re almost there. I’m trying really hard to make the C++ standard more palatable here, so bear with me. Two types T1\mathtt{T1} and T2\mathtt{T2} are called similar if they have cv-decompositions of equal size such that each of their respective PiP_i’s are either (1) the same, or (2) one is “array of NiN_i” and the other is “array of unknown size of”; and, moreover, their U\mathtt{U}’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 T1\mathtt{T1} and T2\mathtt{T2} be two types. Then, their cv-combined type T3\mathtt{T3}, if it exists, is a type similar to T1\mathtt{T1} such that, for each i>0i>0:

  • cvi3=cvi1cvi2cv_i^3=cv_i^1\cup cv_i^2