c++arraysc++17polymorphismtemporary-objects

Initialize polymorphic C array with references/pointers to temporary statically allocated objects in C++


Question

Is it possible to initialize a polymorphic C array (e.g. an array of type ParentClass*, containing pointers of type Subclass1*, Subclass2* etc.) with references/pointers to temporary objects in C++? As all temporary objects are known at compile time, I don't want to use unique_ptr or anything with a dynamic allocation, e.g. using new.

Use case

Current version

Given

class Command {
 public:
  virtual void run(const vector<string>& args) = 0;
};

class Generate: public Command {
 public:
  void run(const vector<string>& args) override;
};

class Scan: public Command {
 public:
  void run(const vector<string>& args) override;
};

I want to define a class App holding a static Command* commands[] data member, e.g.:

// App.hpp
class App {
  static Command* commands[];
}

// App.cpp
Generate generate;
Scan scan;
Command* App::commands[] {&generate, &scan};

Compiler errors

While trying to find a working solution, I encountered the following list of compiler errors:

taking the address of a temporary object of type 'Generate':
  Command* App::commands[] {&Generate()};`
non-const lvalue reference to type 'Generate' cannot bind to a temporary of type 'Generate'
  Command* App::commands[] {&static_cast<Generate&>(Generate())};
cannot initialize an array element of type 'Command *' with an rvalue of type 'const Generate *'
  Command* App::commands[] {&static_cast<const Generate&>(Generate())};

Compiler: g++: Apple clang version 15.0.0 (clang-1500.0.29.1).

Sought changes

In App.cpp it'd love to remove the global variables generate and scan and use temporary objects instead, something like:

// App.cpp
Command* App::commands[] {&Generate(), &Scan()};

even better, get rid of App.cpp and define commands directly in App.hpp, e.g.:

// App.hpp
class App {
  static Command* commands[] {&Generate(), &Scan};
}

// App.cpp is deleted

Solution

  • Yes, but not with pointers. It can work only with arrays to aggregate classes containing a reference member:

    struct CommandRef {
        Command&& command;
    };
    
    /* DO NOT USE, SEE BELOW */
    
    CommandRef App::commands[] {{Generate()}, {Scan()}};
    

    However, I strongly advice against this, because the above relies on very particular lifetime extension rules for binding of references in aggregate initialization.

    It can easily be made to have undefined behavior by mistake, especially if lacking understanding of the formal lifetime rules, for example by

    Compilers have (at least in the past) also had bugs that caused the lifetime extension to not be correctly implemented, so that effectively the code would again have undefined behavior on them.

    Instead use your original approach with named variables or just put the objects directly in the array using a std::variant:

    std::variant<Generate,Scan> App::commands[] {Generate(), Scan()};
    

    even better, get rid of App.cpp and define commands directly in App.hpp

    That's a separate issue and easy in C++17 and later: You just have to put inline before/after static and then it will be possible to initialize the static data member in-class the same way it would work outside of it.