Moving a shared_ptr<StructA> into a shared_ptr<variant<StructA, StructB>>

Viewed 745

I'm trying to avoid a rather complex inheritance chain for a set of plain old data structs, but I need to store them all in a vector + share ownership with the caller.

Struct A {};
Struct B {};

using TInstruction = std::variant<A, B>;

struct holder {
  std::vector<std::shared_ptr<TInstruction>> items;
};

static holder h;

// Every individual TInstruction gets its own add function, for validation purposes
void add(std::shared_ptr<A> a) {
  // Somehow add this to h, while still sharing ownership with the caller
  // h.emplace_back(???)
}


int main() {
  holder h;

  auto a = std::make_unique<A>();
  auto b = std::make_unique<B>();

  add(a);
  // add(b) // not implemented
}

I've had moderate (but annoyingly poor) success with the following changes to the original idea:

  1. Use shared pointers inside the variant, i.e. using TInstruction = std::variant<std::shared_ptr<A>, std::shared_ptr<B>>
  2. Accepting a std::weak_ptr in add() and use .lock() to turn it into a std::shared_ptr

I don't mind #2 (it seems to me that it might be the right way to do it), but keeping the shared_ptr inside the variant inside of "outside" of it leads to some very verbose code and pattern matching.

Is it possible to do this at all? I essentially want to change the type of the shared pointer, but still express the idea of shared ownership.

2 Answers

Instead of using variant, you could take advantage of shared_ptr<void> being able to hold a shared_ptr to anything, as long as you keep track of the type it is holding yourself, as in:

// Generic typelist
template <typename...>
struct Types;

// Find the 0-based index of type T in Types<...>
template <typename, typename>
struct Index;

// T is the first type in Types<...>
template <typename T, typename... Us> 
struct Index<T, Types<T, Us...>> : std::integral_constant<int, 0> {}; 

// T is not the first type in Types<...>
template <typename T, typename U, typename... Us> 
struct Index<T, Types<U, Us...>>
    : std::integral_constant<int, 1 + Index<T, Types<Us...>>()> {}; 

template <typename... Ts> 
struct SharedPtrVariant {
    template <typename T>
    explicit SharedPtrVariant(std::shared_ptr<T> p)
        : sp(std::move(p)), index(Index<T, Types<Ts...>>()) {}

    template <typename T>
    std::shared_ptr<T> get() const {
        return std::static_pointer_cast<T>(
            Index<T, Types<Ts...>>() == index ? sp : nullptr);
    }   

   private:
    std::shared_ptr<void> sp; 
    int index;
};

As per the comments, this is a somewhat silly question because the variant has to be the owner of the object for the final type to be shared_pointer<variant>. Nevin's suggestion would work but uses the void type, which has its own problems (perhaps one could use the any type? that wouldn't be better.)

In the end I'm looking to keep as much type-safety as possible and also make the ownership model clear, so I will settle for a variant of shared pointers.

Related