According to the C++23 standard, what is the output of this program?
#include <iostream>
void f(float &&) { std::cout << "f"; }
void f(int &&) { std::cout << "i"; }
template <typename... T>
void g(T &&... v)
{
(f(v), ...);
}
int main()
{
g(1.0f, 2);
}f are named rvalue references.if
The function template g instantiates to void g, which sequences the call to f(v1) before f(v2). However, f(v1) (the float) calls the int&& overload, and f(v2) (the int) calls the float&& overload!
f(v1) can't call the float&& overload, because that would bind an rvalue reference to an lvalue. It can, however, call the int&& overload by converting the float to a prvalue int. So the float call calls the int overload and vice versa.
First, the template parameter pack is deduced as float, int, and the function parameter pack (T &&... v) is expanded to (float&& v1, int&& v2), since 1.0f is a floating-point literal and 2 is an integer literal.
Inside g, the named rvalue references v1 and v2 are treated as lvalues. §[basic.lval]¶note-3:
In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues (...).
The body of g consists of the fold-expression (f(v), ...) which is a unary right fold with f(v) as a cast-expression and , as a fold-operator. §[expr.prim.fold]¶2:
An expression of the form (... op e) where op is a fold-operator is called a unary left fold. An expression of the form (e op ...) where op is a fold-operator is called a unary right fold.
The instantiation of the unary right fold produces f(v1), f(v2). §[temp.variadic]¶10.2:
The instantiation of a fold-expression (§[expr.prim.fold]) produces:
— (...)
— E1 op (... op (EN-1 op EN)) for a unary right fold,
Now we need to do overload resolution for f, based on the arguments in the two calls. First of all, int&& is reference-related to int&&, and float&& is reference-related to float&&.
Given types “cv1
T1” and “cv2T2”, “cv1T1” is reference-related to “cv2T2” ifT1is similar (§[conv.qual]) toT2, orT1is a base class ofT2.
Two types
T1andT2are similar if they have qualification-decompositions with the same n such that corresponding Pi components are either the same or one is “array of Ni” and the other is “array of unknown bound of”, and the types denoted byUare the same.
A qualification-decomposition of a type
Tis a sequence of cvi and Pi such thatTis“cv0 P0 cv1 P1 ... cvn-1 Pn-1 cvn
U” for n ≥ 0,where each cvi is a set of cv-qualifiers (§[basic.type.qualifier]), and each Pi is “pointer to” (§[dcl.ptr]), “pointer to member of class Ci of type” (§[dcl.mptr]), “array of Ni”, or “array of unknown bound of” (§[dcl.array]).
Since T1 is reference-related to T2, we can't initialize the rvalue reference with an lvalue. §[dcl.init.ref]¶5.4.4:
If
T1is reference-related toT2:— (...)
— if the reference is an rvalue reference, the initializer expression shall not be an lvalue.
This means we can't directly bind the reference to the initializer expression here. We can however implicitly convert the float to a prvalue int using a standard conversion sequence (floating-integral conversion) and bind the int && to it.
To get the "intuitively expected" order of the function calls we can simply use std::forward and rewrite fold-expression like this: (f(std::forward.