iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍣

Handling Structs (Classes) with Union Members

に公開

Preface

This article is a slightly revised version of a post I previously published on Qiita (which has been deleted because the account was banned).
I brought it over because I might write related articles here on Zenn.


The topic mentioned in the title is surprisingly difficult. Since the C era, when one thinks of union...

https://github.com/marudedameo2019/struct_with_union/blob/2dcf71aa90a18876ac92db03f2a7f3194500a5e3/hoge.cpp

I thought it didn't have much of a role, other than being used to check memory alignment like this. However, std::expected, added in C++23, seems to be a Rust Result equivalent, and when I tried to implement a simplified version in C++11, I realized I needed a union.

And I got quite stuck.

1. What is std::expected?

https://cpprefjp.github.io/reference/expected/expected.html

Basically, it's a type that returns both a result and an error at the same time. Modern languages often lack exceptions, and for some people, they are viewed as sworn enemies, much like null. Therefore, types that handle results and errors together are highly valued, allowing for meticulous conditional branching like in C, or stream processing in conjunction with errors. Such types include Rust's Result/Option, or std::expected (C++23)/std::optional (C++17).

2. Why is union necessary?

We handle results and errors together, but they do not exist simultaneously. You could allocate memory for each separately, but that would be wasteful, which is where union comes in (C++17 has std::variant, but C++11 does not).

The first code I wrote was the following, as I wanted to use union like this:

https://github.com/marudedameo2019/struct_with_union/blob/0b98b0a2f80c5d138f8b0428cef32c0ebd403e75/div.cpp

However, this results in an error.

3. Why did it result in an error?

A union does not automatically generate a default constructor.

div.cpp: In function 'result<double, std::__cxx11::basic_string<char> > idiv(int, int)':
div.cpp:16:28: error: use of deleted function 'result<double, std::__cxx11::basic_string<char> >::result()'
   16 |     result<double, string> r;
      |                            ^
div.cpp:5:8: note: 'result<double, std::__cxx11::basic_string<char> >::result()' is implicitly deleted because the default definition would be ill-formed:
    5 | struct result {
      |        ^~~~~~

The primitive union I wrote first was fine, but when a union contains members like objects that have constructors, the default constructor doesn't know which member's constructor needs to be called. This is because only one of them can be called.

4. Creating a constructor for the union

So, first, I'll create a constructor for the union and prepare a constructor on the result side to call it.

https://github.com/marudedameo2019/struct_with_union/blob/fd7a3b090a8d4d2e1e6182a14619123e9aad847c/div.cpp

I thought this would work, but it still throws an error.

div.cpp: In function 'result<double, std::__cxx11::basic_string<char> > idiv(int, int)':
div.cpp:20:29: error: use of deleted function 'result<double, std::__cxx11::basic_string<char> >::~result()'
   20 |     if (right == 0) {return string("cannot divide by 0");} // Changed
      |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
div.cpp:5:8: note: 'result<double, std::__cxx11::basic_string<char> >::~result()' is implicitly deleted because the default definition would be ill-formed:
    5 | struct result {
      |        ^~~~~~

This time it's complaining that there is no default destructor. That's right. Since a union doesn't have a default destructor, the result struct that aggregates it also lacks a default destructor.

5. Calling the Destructor from Outside the union

As such, we provide a placeholder empty destructor inside the union and directly call the destructor for the specific member from within the result destructor. This is necessary because the union itself does not store information about which member was used for construction.

https://github.com/marudedameo2019/struct_with_union/blob/d450d7e569afd7e67bbc5a0b921dffae90636fae/div.cpp

However, even after compiling, it still results in an error.

div.cpp: In function 'result<double, std::__cxx11::basic_string<char> > idiv(int, int)':
div.cpp:25:29: error: use of deleted function 'result<double, std::__cxx11::basic_string<char> >::result(const result<double, std::__cxx11::basic_string<char> >&)'
   25 |     if (right == 0) {return string("cannot divide by 0");}
      |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
div.cpp:5:8: note: 'result<double, std::__cxx11::basic_string<char> >::result(const result<double, std::__cxx11::basic_string<char> >&)' is implicitly deleted because the default definition would be ill-formed:
    5 | struct result {
      |        ^~~~~~

This time, it is complaining that there is no copy constructor for result. While a temporary object is constructed as a return value, it cannot be copied into the variable on the caller's side. This is because, as expected, a copy constructor is also not automatically generated for a union.

6. Defining a Copy Constructor and placement new

We define a copy constructor, but this copy is not straightforward. Since you cannot switch which union member initializer to call based on the original member variable, member initializers cannot be used. Therefore, we do the following:

https://github.com/marudedameo2019/struct_with_union/blob/baa75975e4ccb7e18cda4d0538e2500c5263ec93/div.cpp

Finally, an empty default constructor is included in the union. This is absolutely necessary because member initializers cannot be used. Just like the default destructor, both are a necessary evil.

This finally works and produces:

0.333333

All's well that ends well.

7. Summary

Including non-primitive union members in a class/struct is quite a challenge because default constructors and other special member functions disappear!

  • union lacks a default constructor
    → Provide a constructor with arguments for the union
  • union lacks a default destructor
    → Directly call the union member's destructor from outside the union
  • union lacks a copy constructor
    → Use placement new from the copy constructor outside the union

Reference Links

https://en.cppreference.com/w/cpp/language/union

GitHubで編集を提案

Discussion