c++c++11legacy-code

Create artificial argv array for legacy parent class accepting main-like arguments in the constructor


I am struggling with the legacy code (refactoring scheduled, but I need to use it now) that has the devicedrivers designed to accept (argc, argv) arguments for its constructors. The base class that I have to use, and for now I cannot change it, looks like this:

class LegacyDriver
{
public:
  LegacyDriver(int argc, char** argv)
  {
    if (argc < 3)
    {
      throw std::runtime_error("nope");
    }
    // some logic with the arguments interpretation
    int var1 = std::stoi(argv[1]);
    int var2 = std::stoi(argv[2]);
    std::cout << "config: " << var1 << ", " << var2 << "\n";
    // etc ...
  }
  virtual ~LegacyDriver() = default;
  // + some public interfaces, not important ...
};

Then my new driver that has to extend this class, needs to somehow pass that argc, argv args down to the base class. So far my colleagues were just playing along:

class MyNewDriver: public LegacyDriver
{
public:
  MyNewDriver(int argc, char** argv): LegacyDriver(argc, argv)
  {}
  ~MyNewDriver() override = default;
};

but this design is clearly not sustainable (e.g. difficult for testing) and I would like to at least provide a meaningful constructor to the new driver, like:

  MyNewDriver(int param1, int param2): LegacyDriver(somehowTransformThemIntoArgcArgv)
  {}

I can't use additional MyNewDriver private fields for that, because parent class has to be constructed before. I thought about the hack like this, with the strings and vector in the global scope:

namespace {
std::string hackyProgramName = "Driver";
std::string hackyVar1;
std::string hackyVar2;
std::vector<const char*> hackyArgv;
auto constructArgv = [](int var1, int var2)
{
  hackyVar1 = std::to_string(var1);
  hackyVar2 = std::to_string(var2);
  hackyArgv = std::vector<const char*>({hackyProgramName.c_str(), hackyVar1.c_str(), hackyVar2.c_str()});
  return const_cast<char**>(hackyArgv.data());
};
}
class MyNewDriver: public LegacyDriver
{
public:
  MyNewDriver(int argc, char** argv): LegacyDriver(argc, argv)
  {}
  MyNewDriver(int param1, int param2): LegacyDriver(3, constructArgv(param1, param2))
  {}
  ~MyNewDriver() override = default;
};

But it just looks so ugly, and argv vector is invalidated with every new instance of my driver.

Is there a better way to do this? Preferably with single lambda.


Solution

  • When you move a vector, pointers into it are still valid. So you can construct the argv vector first then move it to a member:

    class ArgsHolder {
        std::vector<std::string> string_storage;
        std::vector<char*> argv_storage;
    public:
        /*explicit(false)*/ ArgsHolder(std::vector<std::string> v) : string_storage(std::move(v)), argv_storage(string_storage.size() + 1) {
            // Remember the +1 for the nullptr at argv[argc]
            for (std::size_t i = 0; i < string_storage.size(); ++i)
                argv_storage[i] = string_storage[i].data();
        }
        char** argv() noexcept { return argv_storage.data(); }
        int argc() noexcept { return argv_storage.size() - 1u; }
    };
    
    class MyNewDriver: public LegacyDriver
    {
      ArgsHolder args;
    public:
      MyNewDriver(int argc, char** argv): LegacyDriver(argc, argv)
      {}
      MyNewDriver(ArgsHolder args): LegacyDriver(args.argc(), args.argv()), args(std::move(args)) {}
      MyNewDriver(int param1, int param2)
       : MyNewDriver(ArgsHolder({ "Driver", std::to_string(param1), std::to_string(param2) }))
      {}
      ~MyNewDriver() override = default;
    };