Author: Chloé Lourseyre
Editor: Peter Fordham
This article is a little compilation1 of strange behaviors in C++, that would not make a long enough article on their own.
Static casting an object into their own type can call the copy constructor
When you use static_cast
, by defaut (i.e. without optimizations activated) it calls the conversion constructor of the object you are trying to cast into (if it exists).
For instance, in this code.
class Foo;
class Bar;
int main()
{
Bar bar;
static_cast<Foo>(bar);
}
The highlighted expression would call the following constructor (if existent): Foo(const Bar &)
.
So far so good, and there is a good chance that you already knew that.
But do you know what happens if you try to static cast
an object into its own type?
Let’s take the following code:
struct Foo
{
Foo(): vi(0), vf(0) {};
Foo(const Foo & other): vi(other.vi), vf(other.vf) {};
long vi;
double vf;
};
int main()
{
Foo foo1, foo2, foo3;
foo2 = foo1;
foo3 = static_cast<Foo>(foo1);
return 0;
}
And look at the assembly of the highlighted lines
Line 12
mov rax, QWORD PTR [rbp-32]
mov rdx, QWORD PTR [rbp-24]
mov QWORD PTR [rbp-48], rax
mov QWORD PTR [rbp-40], rdx
Line 13
lea rdx, [rbp-32]
lea rax, [rbp-16]
mov rsi, rdx
mov rdi, rax
call Foo::Foo(Foo const&) [complete object constructor]
mov rax, QWORD PTR [rbp-16]
mov rdx, QWORD PTR [rbp-8]
mov QWORD PTR [rbp-64], rax
mov QWORD PTR [rbp-56], rdx
We can see that when we static cast
the object foo1
, it calls the copy constructor of Foo
as if the copy constructor was actually a “conversion constructor of a type into itself”.
(Done using GCC 11.2 x86-64, Compiler Explorer (godbolt.org))
Of course, this behavior will disappear as soon as you put an optimization option in the compiler.
This is typically useless knowledge2 and something you doesn’t encounter often in real life (I happen to have encountered it once, but this was an unfortunate accident)
Static casts can call several conversion constructors
Talking about conversion constructors, they can be transitive when static_cast
is used.
Take the following classes:
struct Foo
{ Foo() {}; };
struct Bar
{ Bar(const Foo & other) {}; };
struct FooBar
{ FooBar(const Bar & other) {}; };
struct BarFoo
{ BarFoo(const FooBar & other) {}; };
We have four types: Foo
, Bar
, FooBar
, and BarFoo
. The conversion constructors say we can convert a Foo
into a Bar
, a Bar
into a FooBar
, and a FooBar
into a BarFoo
.
If we try to execute the following code:
int main()
{
Foo foo;
BarFoo barfoo = foo;
return 0;
}
There is a compilation error on line 4
: conversion from 'Foo' to non-scalar type 'BarFoo' requested
.
However, if we static_cast
foo
into a FooBar
, as such:
int main()
{
Foo foo;
BarFoo barfoo = static_cast<FooBar>(foo);
return 0;
}
The program compiles.
If we now take a look at the assembly code associated with line 4
:
lea rdx, [rbp-3]
lea rax, [rbp-1]
mov rsi, rdx
mov rdi, rax
call Bar::Bar(Foo const&) [complete object constructor]
lea rdx, [rbp-1]
lea rax, [rbp-2]
mov rsi, rdx
mov rdi, rax
call FooBar::FooBar(Bar const&) [complete object constructor]
lea rdx, [rbp-2]
lea rax, [rbp-4]
mov rsi, rdx
mov rdi, rax
call BarFoo::BarFoo(FooBar const&) [complete object constructor]
There are no less than 3 conversions generated by that single statement.
(Done using GCC 11.2 x86-64, Compiler Explorer (godbolt.org))
Hold up!
You may be wondering why I didn’t cast foo
into a BarFoo
and I only cast it into a FooBar
using the static_cast
.
If we try and compile the following code:
int main()
{
Foo foo;
BarFoo barfoo = static_cast<BarFoo>(foo);
return 0;
}
We end up with a compilation error!
<source>:16:44: error: no matching function for call to 'BarFoo::BarFoo(Foo&)'
In fact, static_cast is not transitive
What really happens is the following:
The expression static_cast<FooBar>(foo)
tries to call the following constructor: FooBar(const Foo&)
. However, it doesn’t exist, the only conversion constructor FooBar
has is FooBar(const Bar&)
. But, there is a conversion available from Foo
to Bar
, so the compiler implicitly converts foo
into a Bar
to call the FooBar(const Bar&)
.
Then we try to assign the resulting FooBar
to a BarFoo
. Or, more precisely, we try to construct a BarFoo
using a FooBar
, which calls the BarFoo(const FooBar&)
constructor.
That is why there is a compilation error when we try to cast a Foo
directly into a BarFoo
.
In fact, static_cast
is not really transitive.
What to do with this information?
Implicit conversion can happen anywhere. Since static_cast
(and any cast) is, pragmatically3, a “function call” (in the sense that it takes an argument and returns a value) it gives two opportunities for the compiler to try an implicit conversion.
The behavior of C-style casts
Using C-style casts is a fairly widespread bad practice in C++. It really should have made into this old article A list of bad practices commonly seen in industrial projects.
Many C++ developers don’t understand the intricacies of what C-style casts actually do.
How do casts work in C?
If I remember right, casts in C has three uses.
First, they can convert one scalar type into another, like this:
int toto = 42;
printf("%f\n", (double)toto);
But this can only be used to convert scalar type. If we try to convert a C struct
into another using a cast:
#include <stdio.h>
typedef struct Foo
{
int toto;
long tata;
} Foo;
typedef struct Bar
{
long toto;
double tata;
} Bar;
int main()
{
Foo foo;
foo.toto = 42;
foo.tata = 666;
Bar bar = (Bar)foo;
printf("%l %d", bar.toto, bar.tata);
return 0;
}
We obtain the following compilation error:
main.c:22:5: error: conversion to non-scalar type requested
22 | Bar bar = (Bar)foo;
|
(Source: GDB online Debugger | Code, Compile, Run, Debug online C, C++ (onlinegdb.com))
Second, they can be used to reinterpret a pointer into a pointer of another type, like this:
#include <stdio.h>
typedef struct Foo
{
int toto;
long tata;
int tutu;
} Foo;
typedef struct Bar
{
long toto;
int tata;
int tutu;
} Bar;
int main()
{
Foo foo;
foo.toto = 42;
foo.tata = 666;
foo.tutu = 1515;
Bar* bar = (Bar*)&foo;
printf("%ld %d %d", bar->toto, bar->tata, bar->tutu);
return 0;
}
This prints the following output4:
42 666 0
(Source: GDB online Debugger | Code, Compile, Run, Debug online C, C++ (onlinegdb.com))
And finally, it can be used to add or remove a const
qualifier:
#include <stdio.h>
int main()
{
const int toto = 1;
int * tata = (int*)(&toto);
*tata = 42;
printf("%d", toto);
return 0;
}
This prints 42
.
(Source: GDB online Debugger | Code, Compile, Run, Debug online C, C++ (onlinegdb.com))
This also works on struct
s.
And that’s pretty much all5.
So what happens in C++
C++ has its own cast operators (mainly static_cast, dynamic_cast, const_cast, and reinterpret_cast, but also many other casts like *_pointer_cast, etc.)
But C++ was also intended to be backward-compatible with C (at first). So we needed a way to implement the C-style casts so that they would work similarly to C casts, all in the C++ new way of casting.
So in C++, when you do a C-style cast, the compiler tries each one of the five following cast operations (in that order), and uses the first that works:
const_cast
static_cast
static_cast
followed byconst_cast
reinterpret_cast
reinterpret_cast
followed byconst_cast
More details here: Explicit type conversion – cppreference.com.
Why this is actually bad?
Most C++ developers agree that it is really bad practice to use C-style casts in C++. Here are the reasons why: it is not explicit what the compiler will do. The C-style cast will often work, even when there is an error, and silence that error. You always want only one of these casts, so you should explicitly call it. That way, if there is any mistake there’s a good chance the compiler will catch it. Objectively, there are absolutely no upsides to using a C-style cast.
Here is a longer argument against C-style casts: Coding Standards, C++ FAQ (isocpp.org).
Wrapping up
Casting is a delicate operation. It can be costly (more than you think because it gives room for implicit conversions) and still today, there are a lot of people using C-style without knowing how bad it is.
It is tedious, but we need to understand of casts work and the specificities of each one.
Thanks for your attention and see you next time!
Author: Chloé Lourseyre
Editor: Peter Fordham
Addenda
Online compilers links
Static casting an object into their own type can call the copy constructor
Static casts can call several conversion constructors
The behavior of C-style casts
- Trying to cast a struct into another in C (onlinegdb.com)
- Pointer casting in C (onlinegdb.com)
- Const cast in C (onlinegdb.com)
Notes
- Pun intended.
- If you know a case where it is useful, please share in the comments.
- In the linguistic field, pragmatics is the study of context (complementary to semantics, which studies the meaning, and many other fields). In terms of programming language, pragmatics can be interpreted as how features interact with others in a given context. In our example, a
static_cast
can hardly be considered a function call in the semantics sense, but act as one in the interactions it has with its direct environment (as it is explained in the paragraph). The technical truth is in-between: for POD it is not a function call, but for classes that define a copy-constructor it is. - I won’t explain in detail why it prints
0
instead of1515
for the value oftutu
: just know that because we reinterpret the data stored in memory, reading aFoo
as if it was aBar
leads to errors. - I am not as fluent in C as I am in C++. I may have forgotten another use of C casts. If so, please contribute in the comments.
Those constructors are invoked because of implicit conversions, not because of the cast itself.
Try to make all constructors “explicit” and see what happens.