Search code examples
c++c++11memory-managementraii

RAII char ** to be used as exec argument vector


I'm currently creating an app to launch external apps. The signature to launch the external apps is:

int launchApp(int argc, char** argv); // argc = amount of arguments, argv = arguments

To add arguments to a std::vector<char *> structure I use the following lambda:

auto addArgument = [](std::vector<char *> & lArguments,
                       const std::string & sArgument)
{
   auto cstr = new char[sArgument.size() + 1];
   std::copy(sArgument.cbegin(), sArgument.cend(), cstr);
   cstr[sArgument.size()] = '\0';
   lArguments.push_back(cstr);
};

And launching an external app:

std::vector<char *> lArguments;
addArgument(lArguments, "Argument 1");
addArgument(lArguments, "Argument 2");
launchApp(lArguments.size(),static_cast<char**>(lArguments.data());
//... Clean up arguments 

How would I do this in a RAII manner instead? I was thinking of using a std::vector<std::vector<char>> instead. However, how can I then pass the underlying raw data (char**) to launchApp()? static_cast<char**>(lArguments.data()) wouldn't work...


Solution

  • I usually do this in two parts:

    1. Build a std::vector<std::string> containing the actual arguments.
    2. Build a std::vector<const char*> where each element is the .data() of the corresponding element in the vector of strings.

    The first vector, and the strings contained within it, handle your allocation, resizing, etc. while the second simply acts as an index into the memory that is being managed by the first. The lifetime of your second vector should be shorter than that of the first, which you can guarantee by wrapping both in a class.

    Example:

    #include <string>
    #include <vector>
    #include <unistd.h>
    
    int main() {
        std::vector<std::string> args = {"echo", "hello", "world"};
        std::vector<const char*> argv;
        argv.reserve(args.size());
        for (auto& arg : args) {
            argv.push_back(arg.data());
        }
    
        execvp("echo", const_cast<char**>(argv.data()));    
    }
    

    (Note the ugly const_cast. Depending on how you look at it, this is either because the exec* family of functions don't follow const correctness, or because std::string::data() does not have a non-const version (prior to C++17)).


    Or, with the class to wrap it all up nicely:

    #include <string>
    #include <vector>
    #include <unistd.h>
    
    class ExecArguments {
     public:
      ExecArguments(std::initializer_list<std::string> arguments)
       : args(arguments) {
           for (auto& arg : args) {
               argv.push_back(const_cast<char*>(arg.data()));
           }
       }
    
       char** data() {
           return const_cast<char**>(argv.data());
       }
    
     private:
      std::vector<std::string> args;
      std::vector<char*> argv;
    };
    
    int main() {
        ExecArguments args{"echo", "hello", "world"};
    
        execvp(args.data()[0], args.data());    
    }