Doc. no.: P0608R1 2017-12-30 LEWG, LWG Zhihao Yuan

A sane variant converting constructor

Problems to solve

1. variant constructs entirely unintended alternatives.
variant<string, bool> x = "abc";             // holds bool
variant<bool, unique_ptr<int>> x = nullptr;  // holds bool

The above holds string and unique_ptr, respectively, with the proposed fix.

1. variant prefers constructions with information losses.
variant<char, optional<char16_t>> x = u'\u2043';  // holds char = 'C'
double d = 3.14;
variant<int, reference_wrapper<double>> y = d;    // holds int = 3

The above preserves the input value in optional<char16_t> and reference_wrapper<double>, respectively, with the proposed fix.

1. variant performs unstable constructions.
using T = variant<float, int>;
T v;
v = 0;    // switches to int
• When T is upgraded to variant<float, long>,
using T = variant<float, long>;
T v;
v = 0;    // error
• When T is upgraded to variant<float, big_int<256>>,
using T = variant<float, big_int<256>>;
T v;
v = 0;    // holds 0.f

In both cases, the proposed fix consistently constructs with the second alternative.

As shown, the problems equally apply to the converting constructor and the converting assignment operator.

Proposed resolution

This paper proposes to constrain the variant converting constructor and the converting assignment operator to prevent narrowing conversions and boolean conversions. This section explains what exactly this change brings.

Lemma 1. Let $X$$X$ be a sum type $T+U$$T+U$. Let $\mathrm{A}=\left\{{T}_{1},{T}_{2},{T}_{3},...\right\}$${\rm A}=\{T_1, T_2, T_3, ...\}$ be a set of types that are convertible to $T$$T$, and $\mathrm{B}=\left\{{U}_{1},{U}_{2},{U}_{3},...\right\}$${\rm B}=\{U_1, U_2, U_3, ...\}$ be a set of types that are convertible to $U$$U$. Let $\mathrm{Y}$${\rm Y}$ be a set of types that are convertible to $X$$X$. $\mathrm{Y}=\mathrm{A}\ominus \mathrm{B}$${\rm Y}={\rm A}\ominus {\rm B}$ (symmetric difference) rather than $\mathrm{A}\cup \mathrm{B}$${\rm A}\cup {\rm B}$, because each ${T}_{i}={U}_{j}$$T_i=U_j$ causes an ambiguity.

Theorem 1. If $\mathrm{A}$${\rm A}$ and $\mathrm{B}$${\rm B}$ are extended to include a type $\tau \in \mathrm{A}\cap \mathrm{B}$$\tau \in {\rm A}\cap {\rm B}$, $\mathrm{Y}$${\rm Y}$ is shrunk.

In short, constraining variant<$Ts...$$Ts...$> (with $\overline{Ts}>1$$\overline{Ts}>1$) converting constructor may enable more types to be convertible to a variant.

Definition 1. For type $T$$T$ that is convertible to ${T}^{\prime }$$T'$, let $\mathrm{P}$${\rm P}$ be a set of all the possible values for $T$$T$, and $\mathrm{Q}$${\rm Q}$ be a set of all the possible values for ${T}^{\prime }$$T'$. If $\mathrm{P}\subseteq \mathrm{Q}$${\rm P}\subseteq{\rm Q}$, ${T}^{\prime }$$T'$ is denoted as ${T}^{+}$$T^+$. Otherwise, $\mathrm{P}⊈\mathrm{Q}$${\rm P}\nsubseteq{\rm Q}$ and ${T}^{\prime }$$T'$ is denoted as ${T}^{-}$$T^-$. The conversion from $T$$T$ to ${T}^{-}$$T^-$ (denoted as $T⇀{T}^{-}$$T\rightharpoonup T^-$) is a potentially unrepresentable conversion.

In this paper, narrowing conversions (considering only the types) and boolean conversions assemble the potentially unrepresentable conversions in C++.

Lemma 2. Potentially unrepresentable conversions in C++ have Conversion rank.

The proof is left as an exercise for the reader.

Let X be variant<$Ts...$$Ts...$>, r be a value of $T$$T$.

When $\overline{Ts}=1$$\overline{Ts}=1$, without loss of generality, X is variant<${T}^{-}$$T^-$>. Effects of the proposed resolution can be summarized as follows:

X v = r; before after
variant<float> v = 0; holds .0f ill-formed
variant<float> v = INT_MAX; holds INT_MAX + 1 ill-formed

However, variant<V> is such a rare variant, as you can hardly say that $V+\mathrm{\perp }$$V+\bot$ is a sum type.

When $\overline{Ts}=2$$\overline{Ts}=2$, X is variant<${T}^{-},U$$T^-,U$>.

• If $T$$T$ is not convertible to $U$$U$, the effects are the same as the case $\overline{Ts}=1$$\overline{Ts}=1$.
• If $T⇀U$$T\rightharpoonup U$ is an Exact Match or a Promotion, $T⇀{T}^{-}$$T\rightharpoonup T^-$ has a worse conversion sequence, $U$$U$ is constructed in X v = r under the existing rule. The proposed resolution constructs $U$$U$ as well because $T⇀{T}^{-}$$T\rightharpoonup T^-$ is not viable.
• If $T$$T$ is potentially unrepresentable converted to $U$$U$, according to Lemma 2, both $T⇀U$$T\rightharpoonup U$ and $T⇀{T}^{-}$$T\rightharpoonup T^-$ have Conversion rank, so X v = r is ill-formed due to ambiguity under the existing rule. Meanwhile, it’s also ill-formed given the proposed resolution, because both conversions are potentially unrepresentable.
• If $T⇀U$$T\rightharpoonup U$ has Conversion rank and is not potentially unrepresentable, X v = r is still ill-formed under the existing rule. However, $U$$U$ is constructed given the proposed resolution.
• If $T⇀U$$T\rightharpoonup U$ is a user-defined conversion, X v = r constructs ${T}^{-}$$T^-$ under the existing rule. With the proposed resolution, it constructs $U$$U$ instead.

The effects are summarized in the order of the bullets:

X v = r; before after
variant<float, vector<int>> v = 0; holds float ill-formed
variant<float, int> v = 'a'; holds int('a') holds int('a')
variant<float, char> v = 0; ill-formed ill-formed
variant<float, long> v = 0; ill-formed holds long
variant<float, big_int<256>> v = 0; holds float holds big_int

When $\overline{Ts}>2$$\overline{Ts}>2$, let $T{s}_{1}={T}^{-}$$Ts_1=T^-$, $S$$S$ be an overload set $\left\{f\left(\tau \right)\mid \tau \in Ts\right\}$$\{f(\tau) \mid \tau \in Ts\}$, ${S}^{\prime }=S-\left\{f\left({T}^{-}\right)\right\}$$S'=S-\{f(T^-)\}$.

• If $S\left(r\right)$$S(r)$ resolves to $f\left({T}^{-}\right)$$f(T^-)$, this is equivalent to the case $\overline{Ts}=1$$\overline{Ts}=1$; or
• if ${S}^{\prime }\left(r\right)$$S'(r)$ resolves to $f\left(U\right)$$f(U)$ where $T⇀U$$T\rightharpoonup U$ has Conversion rank, X v = r is ill-formed without the proposed resolution; or
• if ${S}^{\prime }\left(r\right)$$S'(r)$ resolves to $f\left(U\right)$$f(U)$ where $T⇀U$$T\rightharpoonup U$ is a user-defined conversion, ${S}^{\prime }-\left\{f\left(U\right)\right\}$$S'-\{f(U)\}$ has no viable function against $r$$r$, and the situation is equivalent to the last bullet in the case $\overline{Ts}=2$$\overline{Ts}=2$.
• Otherwise, ${S}^{\prime }\left(r\right)$$S'(r)$ resolves to $f\left(U\right)$$f(U)$ where $T⇀U$$T\rightharpoonup U$ is an Exact Match or a Promotion, X v = r constructs $U$$U$ with or without the proposed resolution.

The effects are summarized in the order of the bullets:

X v = r; before after
variant<float, big_int<256>, big_int<128>> v = 0; holds float ill-formed
variant<float, long, double> v = 0; ill-formed holds long
variant<float, vector<int>, big_int<256>> v = 0; holds float holds big_int
variant<float, int, big_int<256>> v = 'a'; holds int holds int

Theorem 2. For variant<$Ts...$$Ts...$> v = r, where r is a value of $R$$R$, when there exists one and only one $\tau \in Ts$$\tau \in Ts$ rendering $R$$R$ to be potentially unrepresentable converted to $\tau$$\tau$, the proposed resolution may cause breaking changes

• by preventing the initialization if it used to construct $\tau$$\tau$, or
• by preferring a different and user-defined conversion $R⇀U$$R\rightharpoonup U$ when there is exactly one $U\in Ts-\left\{\tau \right\}$$U\in Ts-\{\tau\}$ that is convertible from $R$$R$.

The first case is easy to fix while the second gives desired outcome for this paper. Both behaviors to be changed are bugs, not features, as shown in Section 1.

Alternative designs

The author came up with and experimented a few other designs, here we list two basic ideas.

1. Use the alternatives’ order information in determining which one to construct. The idea defeats the purpose of the converting constructor because if the construction is sensitive to the order of the alternatives declared in the variant template argument list, in_place_index would be a better choice. The converting constructor and assignment operator assume unordered alternatives.

2. Distinguish implicit and explicit conversions. First, the idea doesn’t work well with the converting assignment operator; applying the implicit policy seems to be the only choice to maintain a consistent behavior, but this may be overkill. Second, it is counterintuitive to have an explicit constructor accepting fewer types comparing to an implicit one because of Theorem 1.

Wording

This wording is relative to N4713.

Modify 23.7.3.1 [variant.ctor]/12 as indicated:

template<class T> constexpr variant(T&& t) noexcept(see below );

Let Tj be a type that is determined as follows: build an imaginary function FUN(Ti) for each alternative type Ti, where FUN(Ti) shall not participate in overload resolution unless Ti{t} is well-formed and is not a boolean conversion (7.14). The overload FUN(Ti) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative Tj which is the type of the contained value after construction.

[…]

Modify 23.7.3.3 [variant.assign]/8 as indicated:

template<class T> variant& operator=(T&& t) noexcept(see below );

Let Tj be a type that is determined as follows: build an imaginary function FUN(Ti) for each alternative type Ti, where FUN(Ti) shall not participate in overload resolution unless Ti{t} is well-formed and is not a boolean conversion (7.14). The overload FUN(Ti) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative Tj which is the type of the contained value after assignment.

[…]