Suppose you have a class with two constructors.
struct WidgetBase { // local mode WidgetBase(); // remote mode WidgetBase(std::string const& server); // The mutex makes this non-copyable, non-movable std::mutex m_mutex; }; struct WidgetOptions { ⟦ random stuff ⟧ }; struct Widget : WidgetBase { Widget(WidgetOptions const& options) : // This doesn't work CanBeLocal(options) ? WidgetBase() : WidgetBase(GetServer(options)) {} static bool CanBeLocal(WidgetOptions const&); static std::string GetServer(WidgetOptions const&); };
We want to use the base class’s local constructor if the options are compatible with a local Widget. Otherwise, we have to create a remote Widget. But you can’t choose a base class constructor at runtime. Your constructor has to call the base class constructor somehow, and by that point the decision has already been made.
You might try using the ternary operator.
Widget(WidgetOptions const& options) : WidgetBase( CanBeLocal(options) ? WidgetBase() : WidgetBase(GetServer(options))) {}
But this doesn’t work because it invokes the copy and/or move constructor: The ternary operator produces a WidgetBase
by one means or another, and then we have to copy/move the temporary into the base class WidgetBase
object.
The secret, once again, is to take advantage of copy elision.
struct Widget : WidgetBase { Widget(WidgetOptions const& options) : WidgetBase(ChooseWidgetBase(options)) {} static bool CanBeLocal(WidgetOptions const&); static std::string GetServer(WidgetOptions const&); private: static WidgetBase ChooseWidgetBase( WidgetOptions const& options) { if (CanBeLocal(options)) { return WidgetBase(); } else { return WidgetBase(GetServer(options)); } } };
This looks the same as the ternary, just moved out of line, but it’s subtly different.
The difference is that all of the return
statements use one of the magic copy elision forms: return WidgetBase(⟦...⟧)
. This allows the compiler to construct the WidgetBase
object directly into the return value, and when called from the Widget
constructor, the return value is the WidgetBase
base class object.
If you like throwing everything inline, you can use a lambda to put the helper directly into the base class constructor arguments.
Widget(WidgetOptions const& options) : WidgetBase([&] { if (CanBeLocal(options)) { return WidgetBase(); } else { return WidgetBase(GetServer(options)); } }()) {}
Bonus chatter: The problem with the ternary is that the ternary expression is not a copy elision candidate. The rule for ternary expressions is that the result is initialized from the branch of the ternary that is selected. The value from the branch is copied/moved into the expression result, and it is the result that is constructed in place.
So as far as knows, it's constructing a standalone object, but really it's constructing the sub-object of a in place. That's wild. I'm curious if there are cases in which this abstraction leaks or fails altogether. If the constructor calls a virtual method, it will use the vtable, but that's consistent with the normal C++ rule for vtables of sub-objects during construction. If has a virtual base, the optimization can't work in general because the sub-object might have a different layout from the standalone object; there's some further discussion...
In LLVM bug 34516, according to Richard Smith, this is a bug in standard wording and "guaranteed copy elision" cannot be applied to base subobjects because of layout difference. So your concern of virtual base is very valid.
I think (1) either the current standard truly wants to require guaranteed copy elision for base subobjects, in which case any function returning a class object with a virtual base must know whether it's returning a most derived object or a base subobject (e.g., with a hidden flag or smuggling some information into the target storage pointer), (2) or guaranteed copy elision is...