0
// 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.

user5406764
  • 1,627
  • 2
  • 16
  • 23
  • 1
    It seems that you are asking how to postpone the construction of the `B` object until after it is known that the insertion will succeed. If that's correct, you should be a little bit more clear about it. – Brian Bi Feb 21 '21 at 20:41
  • 1
    You can't store a `B` in a map that has `A`s... so not really sure what the goal is here. – super Feb 21 '21 at 20:49
  • Even though "B has no additional members", it still gets sliced, which means a move/copy constructor of `A` must be invoked with the temporary `B` as argument, and then there is no actual `B` object alive within the `unordered_map`. – aschepler Feb 21 '21 at 20:58
  • Good points all -- I responded with an addendum above. – user5406764 Feb 21 '21 at 21:01

3 Answers3

3

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};}}
};
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

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)

You cannot. As a workaround, you can first check where the node would be inserted, and then if it isn't there, use it as a hint to insert the element:

const auto it = map.lower_bound(5);
if (it == map.end() || it->first != 5) {
    map.insert(it, std::make_pair(5, B{5, 6}));
}

Using the insertion hint avoids having to walk down the tree a second time.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
0

Let's simplify your example a bit.

void foo(bool b, int* a) {
    if (b) return;
    for (int i = 0; i < 1000; ++i)
        a[i] = i + 42;
}

foo (rand() == 42, std::array<int, 1000>().data());

Do you expect that the compiler will omit creation of std::array<int, 1000> if foo fails to use it?

If you want a big array to be created only if foo goes past the first return statement, then it's up to you to create a big array only when foo goes past the first return statement. There are many ways to do that, but hoping that the compiler will magically do it for you is not one of them. You need to change foo so that it creates a big array when it needs one.

Going back to your example, you have asked for creation of a temporary object of type B by writing B{5, 6}, and you've got a temporary object of type B, regardless of whether emplace succeeds or not. This is true for any other object that you pass to emplace.

  map.try_emplace(5, 5);         // 5 is always constructed
  map.try_emplace(5, A{5});      // A{5} is always constructed
  map.try_emplace(5, B{5, 6});   // B{5, 6} is always constructed

If you want to construct a B object only if emplace succeeds, you need to move creation of B to a piece of code that is executed when emplace succeeds. The constructor of A is a good place for that.

Switching to a lazy-evaluation language such as Haskell is another (radical) solution to this problem.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243