How can I choose a different C++ constructor at runtime?

Make somebody else do it, and then use copy elision. The post How can I choose a different C++ constructor at runtime? appeared first on The Old New Thing.

Mar 6, 2025 - 23:06
 0
How can I choose a different C++ constructor at runtime?

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 Widget­Base 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.

The post How can I choose a different C++ constructor at runtime? appeared first on The Old New Thing.