tl;dr:
const_cast<int&>(static_cast<int const&>(f))
is valid c++
(int&)f
should have the same result
- but it doesn't due to an ancient compiler bug that never got fixed
Long Explanation
1. why const_cast<int&>(static_cast<int const&>(f))
works
1.1 the static_cast
Let's start with the static_cast<int const&>(f)
:
Let's check what the result of that cast would be:
7.6.1.9 Static cast (emphasis mine)
(1) The result of the expression static_cast<T>(v)
is the result of converting the expression v
to type T
. If T
is an lvalue reference type or an rvalue reference to function type, the result is an lvalue; if T
is an rvalue reference to object type, the result is an xvalue; otherwise, the result is a prvalue. The static_cast operator shall not cast away constness (expr.const.cast).
int const&
is an lvalue reference type, so the result of the static_cast<>()
must be some sort of lvalue.
Then let's find out what conversion actually happens:
7.6.1.9 Static cast
(4) An expression E
can be explicitly converted to a type T
if there is an implicit conversion sequence (over.best.ics) from E
to T
, [...].
If T
is a reference type, the effect is the same as performing the declaration and initialization
T t(E);
for some invented temporary variable t
([dcl.init]) and then using the temporary variable as the result of the conversion.
- In our case the declaration would look like this:
const int& t(f);
- I'm not going to elaborate the entire conversion process here to keep it short, you can read the exact details in 12.2.4.2 Implicit conversion sequences
- In our case the conversion sequence would consist of 2 steps:
convert the glvalue float to a prvalue (this also allows us to get rid of const
)
7.3.2 Lvalue-to-rvalue conversion (emphasis mine)
(1) A glvalue of a non-function, non-array type T
can be converted to a prvalue. If T
is an incomplete type, a program that necessitates this conversion is ill-formed. If T
is a non-class type, the type of the prvalue is the cv-unqualified version of T
. Otherwise, the type of the prvalue is T
.
Given that float
is of non-class type, this allows us to convert f
from float const&
to float&&
.
convert from float to int
7.3.11 Floating-integral conversions
(1) A prvalue of a floating-point type can be converted to a prvalue of an integer type. The conversion truncates; that is, the fractional part is discarded. The behavior is undefined if the truncated value cannot be represented in the destination type.
So we end up with a nicely converted int
value from f
.
So the final result of the static_cast<>
part is an lvalue int const&
.
1.2 the const_cast
Now that we know what the static_cast<>
part returns, we can focus on the const_cast<int&>()
:
The result type needs to be:
7.6.1.11 Const cast (emphasis mine)
(1) The result of the expression const_cast<T>(v)
is of type T
. If T
is an lvalue reference to object type, the result is an lvalue; if T
is an rvalue reference to object type, the result is an xvalue; otherwise, the result is a prvalue and the lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the expression v
. Conversions that can be performed explicitly using const_cast are listed below. No other conversion shall be performed explicitly using const_cast.
The static_cast<>
resulted in an lvalue, so the result of the const_cast<>
must also be an lvalue.
What conversion does the const_cast<>
do?
7.6.1.11 Const cast (emphasis mine)
(4) For two object types T1
and T2
, if a pointer to T1
can be explicitly converted to the type “pointer to T2
” using a const_cast, then the following conversions can also be made:
(4.1) an lvalue of type T1
can be explicitly converted to an lvalue of type T2
using the cast const_cast<T2&>
;
(4.2) a glvalue of type T1
can be explicitly converted to an xvalue of type T2
using the cast const_cast<T2&&>
; and
(4.3) if T1
is a class type, a prvalue of type T1
can be explicitly converted to an xvalue of type T2
using the cast const_cast<T2&&>
.
The result of a reference const_cast refers to the original object if the operand is a glvalue and to the result of applying the temporary materialization conversion otherwise.
So the const_cast<>
will convert the lvalue const int&
to an int&
lvalue, which will refer to the same object.
1.3 conclusion
const_cast<int&>(static_cast<int const&>(f))
is well-formed and will result in a lvalue int reference.
You can even extend the lifetime of the reference as per 6.7.7 Temporary objects
(6) The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following:
[...]
- (6.6) a
- (6.6.1) const_cast
(expr.const.cast),
[...]
converting, without a user-defined conversion, a glvalue operand that is one of these expressions to a glvalue that refers to the object designated by the operand, or to its complete object or a subobject thereof,
[...]
So this would also be legal:
float const& f = 1.2f;
int& i = const_cast<int&>(static_cast<int const&>(f));
i++; // legal
return i; // legal, result: 2
1.4 notes
- It is irrelevant in this case that the operand of
static_cast<>
is a const float reference, since the lvalue-to-rvalue conversion that static_cast is allowed to perform can strip away const.
So those would also be legal:
int& i = const_cast<int&>(static_cast<int const&>(1.0f));
// when converting to rvalue you don't even need a const_cast:
// (due to 7.6.1.9 (4), because int&& t(1.0f); is well-formed)
// the result of the static_cast would be an xvalue in this case.
int&& ii = static_cast<int&&>(1.0f);
- Because of that the following c-style casts are also well-formed:
float f = 1.2f;
int const& i = (int const&)f; // legal, will use static_cast
int&& ii = (int&&)f; // legal, will use static_cast
2. why (int&)f
doesn't work
You're technically correct in that it should work, because a c-style cast is allowed to perform this conversion sequence:
7.6.3 Explicit type conversion (cast notation)
(4) The conversions performed by
(4.1) a const_cast
(expr.const.cast),
(4.2) a static_cast
(expr.static.cast),
(4.3) a static_cast
followed by a const_cast
,
(4.4) a reinterpret_cast
(expr.reinterpret.cast), or
(4.5) a reinterpret_cast
followed by a const_cast
,
can be performed using the cast notation of explicit type conversion. The same semantic restrictions and behaviors apply, [...].
So const_cast<int&>(static_cast<int const&>(f))
should definitely be a valid conversion sequence.
The reason why this doesn't work is actually a very, very old compiler bug.
According to 7.6.3 [expr.cast] paragraph 4, one possible interpretation of an old-style cast is as a static_cast followed by a const_cast. One would therefore expect that the expressions marked #1 and #2 in the following example would have the same validity and meaning:
struct S {
operator const int* ();
};
void f(S& s) {
const_cast<int*>(static_cast<const int*>(s)); // #1
(int*) s; // #2
}
However, a number of implementations issue an error on #2.
Is the intent that (T*)x
should be interpreted as something like const_cast<T*>(static_cast<const volatile T*>(x))
The resultion was:
Rationale (July, 2009):
According to the straightforward interpretation of the wording, the example should work. This appears to be just a compiler bug.
So the standard agrees with your conclusion, it's just that no compiler actually implements that interpretation.
2.2 Compiler Bug Tickets
There are already open bugs for gcc & clang regarding this issue:
2.3 why isn't this fixed yet after all those years?
I don't know, but given they have to implement a new standard roughly every 3 years now with tons of changes to the language every time it seems reasonable to ignore issues that most programmers probably won't ever encounter.
Note that this is only a problem for primitive types. My guess is that the reason for the bug is that for those the cv-qualifiers can be dropped by a static_cast
/ reinterpret_cast
due to the lvalue-to-rvalue conversion rule.
If T is a non-class type, the type of the prvalue is the cv-unqualified version of T. Otherwise, the type of the prvalue is T.
Note that this bug only affects non-class types, for class-types it'll work perfectly:
struct B { int i; };
struct D : B {};
D d;
d.i = 12;
B const& ref = d;
// works
D& k = (D&)ref;
There will always be a few edge-cases that are not properly implemented in each & every compiler, if it bothers you you can provide a fix & maybe they'll merge it with the next version (at least for clang & gcc).
2.4 gcc code analysis
In the case of gcc a c-style cast currently gets resolved by cp_build_c_cast
:
tree cp_build_c_cast(location_t loc, tree type, tree expr, tsubst_flags_t complain) {
tree value = expr;
tree result;
bool valid_p;
// [...]
/* A C-style cast can be a const_cast. */
result = build_const_cast_1 (loc, type, value, complain & tf_warning,
&valid_p);
if (valid_p)
{
if (result != error_mark_node)
{
maybe_warn_about_useless_cast (loc, type, value, complain);
maybe_warn_about_cast_ignoring_quals (loc, type, complain);
}
return result;
}
/* Or a static cast. */
result = build_static_cast_1 (loc, type, value, /*c_cast_p=*/true,
&valid_p, complain);
/* Or a reinterpret_cast. */
if (!valid_p)
result = build_reinterpret_cast_1 (loc, type, value, /*c_cast_p=*/true,
&valid_p, complain);
/* The static_cast or reinterpret_cast may be followed by a
const_cast. */
if (valid_p
/* A valid cast may result in errors if, for example, a
conversion to an ambiguous base class is required. */
&& !error_operand_p (result))
{
tree result_type;
maybe_warn_about_useless_cast (loc, type, value, complain);
maybe_warn_about_cast_ignoring_quals (loc, type, complain);
/* Non-class rvalues always have cv-unqualified type. */
if (!CLASS_TYPE_P (type))
type = TYPE_MAIN_VARIANT (type);
result_type = TREE_TYPE (result);
if (!CLASS_TYPE_P (result_type) && !TYPE_REF_P (type))
result_type = TYPE_MAIN_VARIANT (result_type);
/* If the type of RESULT does not match TYPE, perform a
const_cast to make it match. If the static_cast or
reinterpret_cast succeeded, we will differ by at most
cv-qualification, so the follow-on const_cast is guaranteed
to succeed. */
if (!same_type_p (non_reference (type), non_reference (result_type)))
{
result = build_const_cast_1 (loc, type, result, false, &valid_p);
gcc_assert (valid_p);
}
return result;
}
return error_mark_node;
}
The implementation is basically:
- try a
const_cast
- try a
static_cast
(while temporarily ignoring potential const mismatches)
- try a
reinterpret_cast
(while temporarily ignoring potential const mismatches)
- if there was a const mismatch in the
static_cast
or reinterpret_cast
variant, slap a const_cast
in front of it.
So for some reason build_static_cast_1
doesn't succeed in this case, so build_reinterpret_cast_1
gets to do it's thing (which will result in undefined behaviour due to the strict aliasing rule)
float
to anint
is not the same thing as converting a reference to afloat
into a reference to anint
. Whether the reference is const-qualified is immaterial. – Ortensiaconst
is a red herring and distracts from the real problem. Consider this simplified complete example : godbolt.org/z/oaxz31j99 – Pentothalconst
is necessary to allow astatic_cast
chain to work; you need to construct a temporary. – Milzie(int&)
toreinterpret_cast
for afloat f;
but expect it tostatic_cast
+const_cast
for aconst float & f
because in the second casef
is a reference type? – Pentothalstatic_cast
+const_cast
in both cases, since there is a viable path (viaint const&
binding to a temporaryint
). But in the latter case it should be blatantly obvious to the compiler that the path exists, since it's the exact same sequence of types with thereinterpret_cast
. – Milzie