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.

timakro
  • 1,739
  • 1
  • 15
  • 31
  • 2
    As an aside, you can store both of those members in an `std::ostream` pointer or ref to avoid the union. – Rakete1111 May 01 '18 at 10:50
  • 3
    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. – Sam Varshavchik May 01 '18 at 10:53
  • `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` 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(new std::ostream(std::cout.rdbuf()))` (otherwise `std::unique_ptr` would try to delete the statically allocated `std::cout`) – timakro May 01 '18 at 13:04

1 Answers1

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.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207