Implementing a move constructor of a tagged union
Asked Answered
L

1

7

I implemented a tagged union using a class containing an anonymous union and a tag:

class LogFile
{
  public:
    LogFile(std::ostream& stdStream);
    LogFile(std::ofstream fileStream);
    LogFile(LogFile&& logFile);
    ~LogFile();

    std::ostream& getStream();

  private:
    enum { STD_STREAM, FILE_STREAM } streamType_;
    union
    {
        std::ostream *stdStream_;
        std::ofstream fileStream_;
    };
};

I have trouble implementing the move constructor. In the overloaded "normal" constructors I know which union member to initialize:

LogFile::LogFile(std::ofstream fileStream)
: streamType_(FILE_STREAM), fileStream_(std::move(fileStream))
{
}

But in the move constructor, how do I know which of stdStream_ or fileStream_ I have to initialize. I can't check the value of streamType_ in the initializer list.

Liven answered 1/5, 2018 at 10:44 Comment(3)
As an aside, you can store both of those members in an std::ostream pointer or ref to avoid the union.Vivacious
If you purpose is to learn how to do proper move construction in tricky situations like this, then that's one thing. But if your real purpose is to get this working, replace this union with std::variant and let it do all the thinking for you.Irritation
std::variant is too new for my taste and not yet supported by my compiler. In my particular case I ended up using a std::unique_ptr<std::ostream> as suggested by @Rakete1111. For stdout you need to create a new instance of std::ostream on the heap with the underlying buffer of std::cout: std::unique_ptr<std::ostream>(new std::ostream(std::cout.rdbuf())) (otherwise std::unique_ptr would try to delete the statically allocated std::cout)Liven
R
8

Unless you do it for practice, replace your tagged union with a std::variant. It's a lot more safe.


Instead of calling a constructor in member initializer list, you can conditionally call it later using placement-new.

Usually, if you don't specify a constructor in member initializer list, a default one is called. But for members of union {...};, no constructor is called at all.

LogFile(LogFile&& other) : streamType_(other.streamType_)
{
    switch (streamType_)
    {
      case FILE_STREAM:
        new (&fileStream_) std::ofstream(std::move(other.fileStream_)); // <--
        break;
      case STD_STREAM:
        stdStream_ = other.stdStream_;
        other.stdStream_ = 0;
        break;
    }
}

Note that you have to manually call the destructor in ~LogFile(), and you need a custom move assignment too. That's why std::variant is better.

Ruttger answered 1/5, 2018 at 10:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.