3 interesting behaviors of C++ casts

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 structs.

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 by const_cast
  • reinterpret_cast
  • reinterpret_cast followed by const_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

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

Notes

  1. Pun intended.
  2. If you know a case where it is useful, please share in the comments.
  3. 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.
  4. I won’t explain in detail why it prints 0 instead of 1515 for the value of tutu: just know that because we reinterpret the data stored in memory, reading a Foo as if it was a Bar leads to errors.
  5. 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.

2 thoughts on “3 interesting behaviors of C++ casts”

  1. Those constructors are invoked because of implicit conversions, not because of the cast itself.
    Try to make all constructors “explicit” and see what happens.

Leave a Reply