【问题标题】:C++11 move constructor for union-like classC ++ 11移动类联合类的构造函数
【发布时间】:2015-03-12 00:57:16
【问题描述】:

有没有更好的方法来为类联合类构建移动构造函数?如果我有一个类似联合的类,比如下面代码中的类,有没有办法构建类或移动构造函数,不需要像下面代码中的移动构造函数那样的 switch 语句。

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;
            }
        }
};

【问题讨论】:

  • 取决于您的编译器。 gcc 支持通过联合进行类型双关,因此无论如何您都可以简单地分配最大的元素
  • 如果您的真实类只包含基本数据类型或 POD 结构,我根本不会费心定义自定义移动构造函数,因为无论如何它只是一个副本。之后不必将源数据类型设置为 0,因为无论如何您都不应该再次读取它。
  • 例外:如果其中一个结构巨大而其他成员很小,则选择性复制/移动可能会有好处。
  • @NeilKirk 在这个类所拥有的union 中不可能有一个指针类型吗?如果是这样,那么移动语义将避免对深拷贝的需要?
  • @JamesAdkison 是的。我假设没有指针欺骗。不过我没有说清楚。

标签: c++ c++11 unions move-constructor


【解决方案1】:

不,没有更好的方法。如果您想安全地从包含任意类型的联合中移动,则必须从最后写入的联合的字段(如果有的话)执行此操作。另一个相反的答案是错误的,请考虑像

这样的例子
union SomethingLikeThisIsGoingToHappenInPractice {
  std::string what_we_actually_want_to_move;
  char what_we_do_not_care_about[sizeof(std::string)+1];
};

如果您在此处使用“最大”类型的移动构造函数,则必须在此处选择 char 数组,尽管移动实际上并没有做任何事情。如果设置了std::string 字段,您希望移动其内部缓冲区,如果您查看char 数组,则不会发生这种情况。还要记住,移动语义是关于语义,而不是关于移动内存。如果这是您的问题,您可以始终使用 memmove 并完成它,不需要 C++11。

这甚至不会涉及从您尚未编写为 C++ UB 的联合成员中读取的问题,即使对于原始类型,但尤其是对于类类型也是如此。

TL;DR 如果您发现自己处于这种情况,请使用 OP 最初提出的解决方案,不是接受的答案中的内容。


PS:当然,如果你只是移动一个只包含可移动的东西的联合,比如原始类型,你可以只使用联合的默认移动构造函数,它只是复制内存;在这种情况下,首先使用移动构造函数确实不值得,除非是为了一致性。

【讨论】:

    【解决方案2】:

    由于联合作为一种数据类型,它在内存中为所有字段引用内存中的同一位置(尽管该空间内较小字段的排序,如 char 数组 [4] 的 4 字节,取决于系统),它是可以使用最大的字段移动联合。这将确保您每次都移动整个联合,而不管您当前将联合用于哪些字段。

    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;
            }
    };
    

    【讨论】:

    • 在带有 Windows 和 Linux 的 x86 和 x86_64 上,int 长度为 32 位,double 长度为 64 位
    • 对,我一定一直在考虑花车 - 我会在答案中更新它,感谢您指出!
    • 我担心如果其中一种类型的移动 ctor 具有自定义行为(例如 std::vector)会怎样。我们不能依靠 move ctor 来表现得像 memcpy,对吗?
    • 我不认为这实际上是标准的。 GCC(可能还有 Clang)通过 C++ 中的联合支持类型双关语,但 C++ 没有指定它。所以,如果你添加这个细节,很好的答案,否则我认为它不正确。
    【解决方案3】:

    是的,有更好的方法。首先它必须添加 EMPTY 标签,之后使用委托复制构造函数:

    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
            }
    };
    

    当然,这个例子不是很有用,不像下面的例子,它使用指向字符串的指针添加工作:

    #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;
    }
    

    【讨论】:

      猜你喜欢
      • 2018-01-23
      • 2012-05-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-12
      • 1970-01-01
      • 2015-06-30
      • 2011-05-22
      相关资源
      最近更新 更多