1

Given the following scenario

template <typename T> struct Parent {};
template <typename T> struct Child : Parent<T> {};

struct Bar
{
  vector<???> stuff;
  Bar(initializer_list<Parent<T>> things)
  {
    // store things as stuff
  }
};

I want to be able to write

Parent<int> parent;
Child<int> child;
Bar bar{parent, child};

My question: what's the best way to polymorphically keep the initializer list? If I wanted to, say, keep it as vector<unique_ptr<Parent<T>>, what would be the best way to copy it from things to stuff?

Dmitri Nesteruk
  • 23,067
  • 22
  • 97
  • 166
  • 4
    With `std::initializer_list>` you'll have [object slicing](https://stackoverflow.com/questions/4403726/learning-c-polymorphism-and-slicing). – Evg Jun 28 '21 at 15:36
  • 1
    As an alternative to using an *initialization list*, you could consider using a [*factory lambda*](https://stackoverflow.com/a/63743699/4641116). The example has a Table with object containers that contain the types, using Sean Parent's static polymorphism technique, which isn't germane to your question -- but may also be an interesting alternative to CRTP polymorphism for your actual code. – Eljay Jun 28 '21 at 15:57
  • See [doc](https://en.cppreference.com/w/cpp/utility/initializer_list): "`std::initializer_list` is a lightweight proxy object that provides access to an array of objects of type `const T`". This means that storing it in any container may lead to situation when copy of this list will point to array which lifetime has ended so you will have UB. – Marek R Jun 28 '21 at 20:30

1 Answers1

3

The first thing to note is that std::initializer_list<Parent<T>> will result in object slicing if some elements are initialized with Child<T> because the underlying array would hold Parent<T> objects.

There could be different solutions. For example, you could use a variadic template for the constructor to avoid slicing:

template<typename T>
struct Bar {
  std::vector<std::unique_ptr<Parent<T>>> stuff;
  
  template<typename... Us>
  Bar(Us... us) {
      stuff.reserve(sizeof...(Us));
      (stuff.push_back(std::make_unique<Us>(std::move(us))), ...);
  }
};

Here, (stuff.push_back(), ...) is a fold-expression that will be expanded into a sequence of push_back calls for each element of the us pack. Due to the const-ness of the initializer_list underlying array we can't do

Bar(Us... us) : stuff{std::make_unique<Us>(std::move(us))...}
{}

With C++17 deduction guides we can simplify the syntax a little bit.

template<typename> struct TypeTrait {};
template<typename T> struct TypeTrait<Parent<T>> { using Type = T; };
template<typename T> struct TypeTrait<Child<T>>  { using Type = T; };

template<typename... Us>
Bar(Us...) -> Bar<std::common_type_t<typename TypeTrait<Us>::Type...>>;

Now we don't need to specify the template parameter explicitly, it will be deduced:

Parent<int> parent;
Child<int> child;
Bar bar{parent, child};   // T deduces to int

(If the member type Type could be put into Parent directly, a helper struct TypeTrait is not needed.)

Evg
  • 25,259
  • 5
  • 41
  • 83
  • This works. The only (tiny) downside is you have to init the thing as Bar(a, b, c) rather than Bar(a, b, c) — would be great if the thing could infer the type argument. Sadly seems it cannot. – Dmitri Nesteruk Jun 28 '21 at 20:04
  • 1
    @DmitriNesteruk `std::vector` should know the type of elements to hold, and this type should be specified as a template parameter. What you could do is to introduce a non-template `ParentBase`, derive `Parent` from it and then use `std::vector>` as a container. If the only concern is the syntax (the necessity to add ``) but not the formal template parameter `Bar` has, this could be solved using a deduction guide and some template metaprogramming tricks. – Evg Jun 28 '21 at 20:12