简体   繁体   中英

C++11 move constructor for union-like class

Is there a better way to build a move constructor for a union-like class? If I were to have a union-like class like the class in the following code, is there a way to build the class or the move constructor that wouldn't require switch statement like the move constructor in the following code.

class S {
    private:
        enum {CHAR, INT, DOUBLE} type; // tag
        // anonymous union
        union {
            char c;
            int n;
            double d;
        };

    public:
        // constructor if the union were to hold a character
        AS(const char c) {
            this->tag = AS::CHAR;
            this->c = c;
        }
        // constructor if the union were to hold a int
        AS(const int i) {
            this->tag = AS::INT;
            this->n = i;
        }
        // constructor if the union were to hold a double
        AS(const double d) {
            this->tag = AS::DOUBLE;
            this->d = d;
        }

        // Move constructor with switch statement
        S(S &&src) : type(std::move(src.type)) {
            switch(type) {
                case CHAR:
                    this->c = src.c);
                    src.c = 0;
                    break;
                case INT:
                    this->n = src.n;
                    src.n = 0;
                    break;
                case DOUBLE:
                    this->d = src.d;
                    src.d = 0
                    break;
                default:
                    break;
            }
        }
};

No, there is no better way. If you want to safely move from a union containing arbitrary types, you must do so from the field of the union that has last been written to (if any). The other answer stating the contrary is wrong, consider an example like

union SomethingLikeThisIsGoingToHappenInPractice {
  std::string what_we_actually_want_to_move;
  char what_we_do_not_care_about[sizeof(std::string)+1];
};

If you use the move constructor of the 'largest' type here you'd have to pick the char array here, despite moving that actually not really doing anything. If the std::string field is set you'd hope to move its internal buffer, which is not going to happen if you look at the char array. Also keep in mind that move semantics is about semantics , not about moving memory. If that was the issue you could just always use memmove and be done with it, no C++11 required.

This isn't even going into the problem of reading from a union member you haven't written to being UB in C++, even for primitive types but especially for class types.

TL;DR if you find yourself in this situation use the solution originally proposed by the OP, not what's in the accepted answer.


PS: Of course if you are just moving a union that only contains things that are trivially moveable, like primitive types, you could just use the default move constructor for unions which does just copy the memory; In that case it's really not worth it to have a move constructor in the first place though, other than for consistencies sake.

Since unions as a data type refer to the same place in memory for all of the fields inside (although the ordering of smaller fields like the 4 bytes of char array[4] within that space depends on the system), it is possible to just move the union using the largest field. This will ensure that you move the entire union every time, regardless of which fields you are currently using the union for.

class S {
    private:
        enum {CHAR, INT, DOUBLE} type; // tag
        // anonymous union
        union {
            char c;
            int n;
            double d;
        };
    public:
        // Move constructor with switch statement
        S(S &&src) : type(std::move(src.type)) {
             this->d = src.d;
             src.d = 0;
        }
};

Yes, there is better way. At first it have to add EMPTY tag, after this use delegating copy constructor:

class S {
    private:
        enum {EMPTY, CHAR, INT, DOUBLE} type; // tag
        // anonymous union
        union {
            char c;
            int n;
            double d;
        };

    public:
        S(){ this->type = EMPTY; }

        // constructor if the union were to hold a character
        S(const char c) {
            this->type = CHAR;
            this->c = c;
        }
        // constructor if the union were to hold a int
        S(const int i) {
            this->type = INT;
            this->n = i;
        }
        // constructor if the union were to hold a double
        S(const double d) {
            this->type = DOUBLE;
            this->d = d;
        }

        S(const S& src) = default;// default copy constructor

        // Move constructor 
        S(S&& src) 
          : S(src) // copy here
        {
          src.type = EMPTY; // mark src as empty
        }
};

Of course, this example is not very useful, unlike the example below, which adds work with a pointer to a string:

#include <cassert>
#include <iostream>
#include <memory>
#include <string>

class S {
    public:
        enum Tag {EMPTY, CHAR, INT, DOUBLE, STRING};
    private:
        Tag type; 
        // anonymous union
        union {
            char c;
            int n;
            double d;
            std::string* str;
        };

    public:
        S(){ this->type = EMPTY; }

        // constructor if the union were to hold a character
        S(const char c) {
            this->type = CHAR;
            this->c = c;
        }
        // constructor if the union were to hold a int
        S(const int i) {
            this->type = INT;
            this->n = i;
        }
        // constructor if the union were to hold a double
        S(const double d) {
            this->type = DOUBLE;
            this->d = d;
        }

        S(std::unique_ptr<std::string> ptr) {
            this->type = STRING;
            this->str = ptr.release();
        }

        std::unique_ptr<std::string> ExtractStr()
        {
           if ( this->type != STRING )
             return nullptr;

           std::string* ptr = this->str;
           this->str  = nullptr;
           this->type = EMPTY;
           return std::unique_ptr<std::string>{ptr};
        }

        Tag GetType() const
        {
          return type;
        }

    private:
        // only move is allowed for public
                     S(const S& src) = default;// default copy constructor
        S& operator = (const S& src) = default;// default copy assignment operator
    public:

        // Move constructor 
        S(S&& src) 
          : S(src) // copy here (but we copy only pointer for STRING)
        {
          src.type = EMPTY; // mark src as empty
        }

        S& operator = (S&& src)
        {
          if ( this->type == STRING )
             ExtractStr();// this call frees memory

          this->operator=(src);
          src.type = EMPTY;
          return *this;
        }

      ~S()
       {
          if ( this->type == STRING )
          {
             ExtractStr();// this call frees memory
          }
       }
};

// some test
int main()
{
  S sN(1);
  S sF(2.2);
  S x{std::move(sN)};

  std::unique_ptr<std::string> pStr1 = std::make_unique<std::string>("specially for Daniel Robertson");
  std::unique_ptr<std::string> pStr2 = std::make_unique<std::string>("example ");
  std::unique_ptr<std::string> pStr3 = std::make_unique<std::string>("~S() test");

  S xStr1(std::move(pStr1)); 
  S xStr2(std::move(pStr2)); 
  S xStr3(std::move(pStr3)); 
  x = std::move(xStr1);

  assert(xStr1.GetType() == S::EMPTY);
  assert(x.GetType() == S::STRING);

  std::string str2 = *xStr2.ExtractStr();
  std::cout << str2 << *x.ExtractStr() << std::endl; 
  return 0;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM