1

I am writing a template class that manages a union of 2 classes. All the functions are pretty simple except the Get() functions. It looks like this:

UnionPair.hpp

template <class First, class Second>
class UnionPair {
 public:
  UnionPair() : state_(State::kEmpty){};
  ~UnionPair(){};

  void Reset();

  void Set(std::unique_ptr<First>&& first);
  void Set(std::unique_ptr<Second>&& second);

  template <First>
  First *Get();

  template <Second>
  Second *Get();

 private:
  enum class State { kEmpty, kFirst, kSecond } state_;
  union {
    std::unique_ptr<First> first_;
    std::unique_ptr<Second> second_;
  };
};

UnionPair.cpp

template <class First, class Second>
template <First>
First *UnionPair<First, Second>::Get() {
  return first_.get();
}

template <class First, class Second>
template <Second>
Second *UnionPair<First, Second>::Get() {
  return second_.get();
}

I'm trying to make the Get() functions template methods that can only be called with the template classes of it's instantiated UnionPair object, so that they can be overloaded by the template class they're called with. The above code does not compile.

My idea of how it would be called is this, but I get a compiler error when I try to call them:

// Definitions of structs A, B and C here

UnionPair<A, B> pair;
pair.Set(std::unique_ptr(new A()));
pair.Get<A>(); // should return a pointer to A (causes a compiler error right now)
pair.Get<C>(); // should cause a compiler error

I'm not really sure how to implement this method in the .cpp file, or if it's even possible. I've been reading about specialization within templates, but haven't seen any examples similar to mine, or examples where the specialization class MUST be the same as the object's template classes.

What is wrong with my usage/definition? Is what I'm trying to do possible? If not, are there any alternatives? What I DON'T want to do is have a seperate getter for First and Second like First *GetFirst() because that relies on the order of the classes in the object instantiation and isn't clear.

Side note: If there is a c++ library that manages the state, setting and getting of union members I'd consider using it, but I'd still like to figure my question out.

  • 1
    The C++ standard library class template that handles this is [std::variant](https://en.cppreference.com/w/cpp/utility/variant) – Nathan Pierson Jul 24 '21 at 01:14
  • Interesting. Looks like it's what I'm looking for, but I think it throws a runtime error if you try and get the wrong type from it? Would that mean there's no way to get a compiler error? Also I can't use c++ 17 in my project, so I can't use this class. But thanks for pointing it out! – Jedediah Heal Jul 24 '21 at 01:19
  • Putting template code into `.cpp` file like this will not work. You are making a very common mistake of writing a huge pile of code up front, and only then trying to make everything work. Professional C++ developers don't work that way. They write only a few lines of code, compile, test it, make sure they work, then write a few more lines. You should start with a template with just a few lines of code, get all of that working, then write a few more lines of template code. – Sam Varshavchik Jul 24 '21 at 01:19
  • I don't really see how the 30 lines of code I wrote is a 'huge pile'... – Jedediah Heal Jul 24 '21 at 01:21
  • I don't think you will be able to get a compiler error when the wrong one of A or B is used, because the compiler may not know which has been set after the object was constructed, but you can get a compiler error when C is used — `get(variant)` should be an error. – jtbandes Jul 24 '21 at 01:25
  • Right okay that's what I'm looking for. I realize it's impossible for the compiler to know the 'state of the union' (hehe), but at least it will compiler assert if C is used. – Jedediah Heal Jul 24 '21 at 01:29

1 Answers1

2

First off, you can't split template code into separate .h and .cpp files:

Why can templates only be implemented in the header file?

Second, a union's data is not set until runtime, so there is no way for the compiler to validate the template parameter of Get() at compile-time, at least not the way you want. It is possible to validate at compile-time only whether the union could never convert to the specified type at all, but if it could convert then you can't validate that until runtime, after the union has been assigned.

Try something like this:

#include <memory>
#include <type_traits>

template <class First, class Second>
class UnionPair {
 static_assert(!std::is_same<First, Second>::value, "First and Second can't be the same type");

 public:
  UnionPair();
  UnionPair(std::unique_ptr<First>&& value);
  UnionPair(std::unique_ptr<Second>&& value);
  ~UnionPair(){}

  void Reset();

  void Set(std::unique_ptr<First>&& value);
  void Set(std::unique_ptr<Second>&& value);

  template <class T>
  T* Get();

 private:
  enum class State { kEmpty, kFirst, kSecond } state_;

  union {
    std::unique_ptr<First> first_;
    std::unique_ptr<Second> second_;
  };

  template<class T>
  typename std::enable_if<std::is_same<T, First>::value, T*>::type
    InternalGet();

  template<class T>
  typename std::enable_if<std::is_same<T, Second>::value, T*>::type
    InternalGet();
};

template <class First, class Second>
UnionPair<First, Second>::UnionPair()
    : state_(State::kEmpty)
{
}

template <class First, class Second>
UnionPair<First, Second>::UnionPair(std::unique_ptr<First>&& value)
    : UnionPair()
{
    Set(std::move(value));
}

template <class First, class Second>
UnionPair<First, Second>::UnionPair(std::unique_ptr<Second>&& value)
    : UnionPair()
{
    Set(std::move(value));
}

template <class First, class Second>
void UnionPair<First, Second>::Reset()
{
  if (state_ == State::kFirst)
    first_.reset();
  else if (state_ == State::kSecond)
    second_.reset();
  state_ = State::kEmpty;
}

template <class First, class Second>
void UnionPair<First, Second>::Set(std::unique_ptr<First>&& value)
{
    Reset();
    first_ = std::move(value);
    state_ = State::kFirst;
}

template <class First, class Second>
void UnionPair<First, Second>::Set(std::unique_ptr<Second>&& value)
{
    Reset();
    second_ = std::move(value);
    state_ = State::kSecond;
}

template <class First, class Second>
template <class T>
typename std::enable_if<std::is_same<T, First>::value, T*>::type
  UnionPair<First, Second>::InternalGet() {
    if (state_ == State::kFirst)
        return first_.get();
    throw std::domain_error("wrong state");
}

template <class First, class Second>
template <class T>
typename std::enable_if<std::is_same<T, Second>::value, T*>::type
  UnionPair<First, Second>::InternalGet() {
    if (state_ == State::kSecond)
        return second_.get();
    throw std::domain_error("wrong state");
}

template <class First, class Second>
template <class T>
T* UnionPair<First, Second>::Get() {
    return InternalGet<T>();
}
UnionPair<A, B> pair;
pair.Set(std::unique_ptr<A>(new A));
pair.Get<A>(); // OK
pair.Get<B>(); // runtime error
pair.Get<C>(); // compiler error
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Yeah maybe I wasn't clear but I fully understand that it can't cause compiler errors between A and B. I was only asking that using C would cause a compiler error, which your code does. Thank you! – Jedediah Heal Jul 24 '21 at 03:23