Blog 2024 03 27 Should we move from fundamental types?
Post
Cancel

Should we move from fundamental types?

In this blog post, we are going to discuss the intersection of move semantics and fundamental types. Should we move values of fundamental types? Or can we even move them in the first place? Let’s delve into this. But first, let’s stop for a second to remind ourselves what fundamental types are.

In C++, fundamental types, also known as built-in types, are the basic data types provided by the language itself. These types are not composed of other types and are directly supported by the compiler. The fundamental types in C++ include:

  • integer types (bool, signed and unsigned versions of short, int, long, long long)
  • floating-point types (float, double, long double)
  • character types (char, unsigned char, wchar_t, char8_t, char16_t, char32_t)
  • void type

What does move mean in C++?

C++11 introduced move semantics. It lets us efficiently transfer resources from one object to another instead of copying them. This is useful both in terms of efficiency and expressiveness. It is efficient because copying big objects might require a lot of memory and/or time. It’s also expressive because move semantics both lets us communicate that something is not going to be used anymore or with the combination of removing copy constructors, we can also express that there should be no more than one of an instance. Although it does let the creation of several objects of the same type. (If you have a non-copyable Printer class, you can have several instances of it, but you cannot copy a specific printer instance.)

Technically, a move means either the usage of the move constructor or the move assignment operator instead of the copy counterparts. In order to use them, those operators taking rvalue references must be defined or generated by the compiler. It’s out of the scope of this article to see how such an operator is defined well. And that leads to the question of what happens if you want to move a fundamental type, or a plain old data type if you like, such as an int.

Do we expect fundamental types to be movable?

On the one hand, fundamental types have no special member functions, so they have no copy or move constructors or move assignment operators per se either. In that sense, we might not expect them to be movable. On the other hand, we all know that we can copy an int, even though it also doesn’t have a copy constructor as it’s not a class type. So a move might be possible just as well. Let’s dig into this question a bit deeper.

Before getting into the standard itself, let’s have a look at the corresponding type traits.

If we have a look at C++ Reference, it says that a type has the trait is_move_constructible if it can be constructed from an rvalue reference. Likewise, it has the trait is_move_assignable if it’s possible to assign an lvalue reference from an rvalue reference.

Let’s be honest, that didn’t bring us any further.

I skimmed through the standard with the help of different keywords and I couldn’t find any mention of move operations for scalar or fundamental types.

CPlusPlus.com on the other hand does mention scalar types as moveable types.

Raymond Chan on Microsoft’s developer blog clearly states what most would probably expect. C++ primitive types do not have special semantics for move constructor or move assignment. Their move operations are just copies.

What actually happens?

Still, it might be interesting to see with some dummy examples what actually happens.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string>
#include <type_traits>

void object_copy(std::string s) {
    std::cout << "object_copy: " << s << '\n';
}

void object_move(std::string&& s) {
    std::string m(std::move(s));
    std::cout << "object_move: " << m << '\n';
}

void fundamental_copy(int i) {
    std::cout << "fundamental_copy: " << i << '\n';
}

void fundamental_move(int&& i) {
    int m(std::move(i));
    std::cout << "fundamental_move: " << i << '\n';
}

int main()
{
    std::string s{"hello"};
    object_copy(s);
    object_move(std::move(s));
    std::cout << "main: " << s << '\n'; 

    int i{42};
    fundamental_copy(i);
    fundamental_move(std::move(i));
    std::cout << "main: " << i << '\n';
}
/*
object_copy: hello
object_move: hello
main: 
fundamental_copy: 42
fundamental_move: 42
main: 42
*/

We can make the following observations:

  • When we have a moved from std::string, the original instance becomes empty
  • When we have a moved from int, the original instance doesn’t change its value.

That means that while the string’s value was moved, the ints was copied. Just as expected based on the above findings.

If we think this further, it also means that our fundamental_move is extremely inefficient. It first makes a copy of the passed-in parameter and then it makes another copy within its body to m.

You can check on Quick-bench its effects.

Truth be told, if you run a static code analyzer on the code, it’s likely that even it will complain that you shouldn’t call std::move on a built-in type…

So never move a fundamental type?

That’s obviously not the case. I use the word obviously for two reasons:

  • never say never, there are always exceptions!
  • every type is built up from fundamental types in the end!

Think about a string, it’s an array of chars, isn’t it? Just dig deep enough in any type and you’ll find the underlying fundamental types not necessary on the first level, but deeper. It’s a revealing experience to think about this fact. It helps you realize that you can really build any program out of some single constructs. The rest is simply there to make our lives easier.

So the short answer to the above question is that you should move fundamental types as part of bigger types. But you do that anyway probably without realizing it. But what to do when you want to express for a single value that it shouldn’t be copied?

You should wrap it into a class or a struct and delete the copy operations. Simply define the move. You might not gain a lot, but you also make sure that accidental copies will not take place and in addition, you increase the type-safety and expressiveness of your code.

So instead of just passing around an int that represents let’s say your outstanding debt, create a type for it:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Debt {
public:
    Debt(int debt): m_debt(debt) {}
    ~Debt() = default;

    Debt(Debt&) = delete;
    Debt& operator(Debt&) = delete;

    Debt(Debt&&) = default;
    Debt& operator=(Debt&&) = default;
private:
    int m_debt = 0;
};

Notice that we deleted the copy operations and defaulted the move operations so everything is clear to the reader about our intentions and we followed the rule of five.

Conclusion

Today, we discussed whether it makes sense to use move semantics with fundamental types. The answer is that in general, no you should not. It will always be a copy instead of a move, so you don’t gain anything in terms of performance, on the other hand, you might deceive the reader. It seems that the only valid use case to still use move semantics with fundamental types is when you really want to express that a value should be moved. Most probably, in that case, you should wrap that fundamental type into a well-named type with its move operations defined.

Connect deeper

If you liked this article, please

This post is licensed under CC BY 4.0 by the author.