Search code examples
c++c++17emplace

try_emplace doesn't work (as desired) when you have an inheritance structure for the value type?


// A couple of simple structs that inherit AND have different ctor arguments
struct A 
{ 
  A(int) {} 
};

// Note that "B" is a subclass of "A", which is important below
struct B : A 
{
  B(int, char) : A(0) // The 0 doesn't matter
  {}
  // Note that B has no data members that "will be chopped"
};

// A simple map... Note the value type is "A"
// There should be no problem putting B's in here though because they are a subclass of A
// And as noted above, B has no additional members that will be chopped
// (
std::unordered_map<int, A> map;

// This works as expected, because we're trying to emplace an object of type "A"
auto [_, success] = map.try_emplace(
  5, // the key
  6  // the single argument to A's ctor
);
assert(success);

// This compiles BUT:
// 1. It's attempting to overwrite the key used above (5), and so
//    the emplace correctly fails
// 2. Uh oh though -- Obviously a "B" is constructed and destructed though
//    the emplace fails
auto [_, success] = map.try_emplace(
  5,
  B{5, 6} // The map knows the value type as "A", which means I only
          // have the option of passing A's ctor args, not B's.  This doesn't
          // do what I want when I'm emplacing a "B"... In fact, this creates
          // an rvalue of type "B" (duh) and then destructs it when the emplace fails.
          //
          // So my question is:  How can I pass the arguments for B's ctor
          // to a map that expects an "A" (keep in mind B is a subclass of A)
);
assert(!success);

To respond to a few of the posters:

  1. I am aware that B will get sliced if it is stored as an A in the map. That is why I specifically mention that there are no additional data members in B.

  2. Yes, I am absolutely trying to make sure that B does not get constructed if the insert would fail. The only way I could get it to compile was to construct a B then shove it in the map. So that's not the map's fault :)

  3. So the only real difference between B and A is their ctor.


Solution

  • You cannot put a B into an A.

    You can slice a B into an A. Slicing refers to copying the A part of B into an A object,

    In C++ variables are their type. They are not a different type, even if that other type would fit in the storage.

    Pointers and references can refer to the base class component of a class. But values are always what they are.

    There is no way to store a B in that map.

    Now if you are ok with slicing, we can defer the construction of the B until emplace finds a spot for it. The B constructed will then be sliced and the A part moved into storage, then destroyed.

    template<class F>
    struct ctor{
      F f;
      template<class T>
      operator T()&&{
        return std::move(f)();
      }
    };
    template<class F>
    ctor(F)->ctor<F>;
    

    now just:

    auto [_, success] = map.try_emplace(
      5,
      ctor{[&]{return B{5, 6};}}
    };