Value Struct - wrapping values for strong typing

One of my favorite things of C++ is this one simple trick: value structs. This trick consists of a struct that wraps its only field, which type is a primitive one (int, float, ...).

Everything is the same

#include <iostream>

float to_rad(float deg) {
    return deg * 3.14 / 180;
}

float to_deg(float rad) {
    return rad * 180 / 3.14;
}

You may wonder: what's wrong with this code? Nothing, so far. The problem is... you can misuse it. As we are going to see, nothing can stop you from using a value representing a "degree" to be passed to a function that takes a "radiant".

int main() {

    float half_pi = 1.57;

    // `half_pi` is 90 degrees in radians
    float ninety_deg = to_deg(half_pi);
    std::cout << ninety_deg << std::endl;

    // 90... degrees or radians? the compiler doesn't care!
    float what = to_deg(ninety_deg);
    std::cout << what << std::endl;

    return 0;
}

Unfortunately, this issue is more widespread than you think. What if to_deg and to_rad return a special type, instead of a raw float?

Wrapping values

struct Degrees {
    float value;
    Degrees(float v): value(v){}
};

struct Radians {
    float value;
    Radians(float v): value(v){}
};

These two structs look exactly the same, but they are different types, and the compiler will treat them as such. No mixing allowed, this time!

Radians to_rad(Degrees d){
    return d.value * 180 / 3.14;
}

Degrees to_deg(Radians r) {
    return r.value * 3.14 / 180;
}

int main() {
    Radians half_pi = Radians(1.57);

    Degrees ninety_degrees = to_deg(half_pi);
    std::cout << ninety_degrees.value << std::endl;

    // Degrees what = to_deg(ninety_degrees);       // COMPILE ERROR!
    // std::cout << what.value << std::endl;

    return 0;
}

You may now think: "ewww, it's going to be sllloooooooowwwwwww!". Well, gcc and clang recognize this pattern, and will replace Degrees and Radians with the float value - as if they didn't exist.

Under the hood, nothing has changed

Let's compare the generated assembly code, generated by gcc 9.1.0 (compile flags: -O2). In this listing, we are going to examine the difference between the first, float-based version (left side) and the struct-based version (right side).

Note: section .LFE1544 and section .LFE1551 define to_deg(float) and to_deg(Radians). section .LFB1545 and section .LF1552 contain the code that is executed - the "real code"

.LFE1544:                                                     | .LFE1551:
        .size   _Z6to_radf, .-_Z6to_radf                      |         .size   _Z6to_rad7Degrees, .-_Z6to_rad7Degrees
        .p2align 4                                                      .p2align 4
        .globl  _Z6to_degf                                    |         .globl  _Z6to_deg7Radians
        .type   _Z6to_degf, @function                         |         .type   _Z6to_deg7Radians, @function
_Z6to_degf:                                                   | _Z6to_deg7Radians:
.LFB1545:                                                     | .LFB1552:
        .cfi_startproc                                                  .cfi_startproc
        mulss   .LC2(%rip), %xmm0                                       mulss   .LC2(%rip), %xmm0
        cvtss2sd        %xmm0, %xmm0                                    cvtss2sd        %xmm0, %xmm0
        divsd   .LC0(%rip), %xmm0                                       divsd   .LC0(%rip), %xmm0
        cvtsd2ss        %xmm0, %xmm0                                    cvtsd2ss        %xmm0, %xmm0
        ret                                                             ret
        .cfi_endproc                                                    .cfi_endproc

As we can see, the difference is just in the description of the function: the real code is the same. Both versions perform the same multiplications and divisions, and read the same registers.

Conclusion

In this blog post, we looked at value structs, that are structures that wrap a single field, and how we can use them to type-check our code and avoid mixing raw values. We also looked the assembly code, and noticed that the wrappers we introduced do not affect the performance of our code: they are compiled as it we just used the raw values. If you feel that, somehow, this article helped you, feel free to share it! If you have questions, ask on Twitter, or offer me a coffee to let me keep writing these notes!

References: