Search code examples
c++stringconst-cast

Converting a std string to char* using const cast


After days of attempting to create a shell I ask for a bit of help. I have started over 4 or so times with different data structures and plea for a solution to the below problem. I have a string that I need to break into individual arguments and have a pointer to that. I ultimately pass args to an exec function but since i cant seem to fill the args correctly i am getting funny results, here is a simplified version of whats happening

char* args[100];
int counter=0;
string temp = "some text and stuff here";
stringstream s (temp);
while(s>> temp)
{
    cout << "TOKEN " << counter << " =" << temp <<endl;
    args[counter]=const_cast<char *> (temp.c_str());
    counter++;
}
//print the debug info
      for( int ii=0; args[ii] != NULL; ii++ )
    {
      cout << "Argument OUT " << ii << ": " << args[ii] << endl;
    }

This code doesn't work and I cant grasp why. The result stores "here" in every value of args but counter gets changed.

TOKEN 0 =some
TOKEN 1 =text
TOKEN 2 =and
TOKEN 3 =stuff
TOKEN 4 =here
Argument OUT 0: here
Argument OUT 1: here
Argument OUT 2: here
Argument OUT 3: here
Argument OUT 4: here

Solution

  • Probably because the temp object is re-using its internal allocation. When you store the c_str() result you are only storing a memory address. The std::string class does not create a completely new allocation each time you read into it from your string stream, rather it reuses the allocation it already has (if possible).

    Further, using a pointer returned by c_str() after you have done anything else to the std::string object from which it was obtained invokes undefined behavior.1

    If possible, just change args to std::vector<std::string>. If this is not possible then you need to strdup() the pointer returned by c_str() in order to create an entirely new allocation that copies the value of the string at that moment. You must, of course, remember to free() the allocations when you are done.

    Additionally, casting away the const qualifier and writing to the pointer results in undefined behavior.2 At a minimum you need to change args to be const char * args[100];, but I would strongly suggest using a vector of strings instead.


    1 http://en.cppreference.com/w/cpp/string/basic_string/c_str

    The pointer obtained from c_str() may be invalidated by:

    • Passing a non-const reference to the string to any standard library function, or
    • Calling non-const member functions on the string, excluding operator[], at(), front(), back(), begin(), rbegin(), end() and rend().

    2 http://en.cppreference.com/w/cpp/string/basic_string/c_str

    Writing to the character array accessed through c_str() is undefined behavior.


    Based on your comment indicating that you need to use exec(), it sounds like you do need an array of pointers-to-char. However, we can still do this using vectors. You need one vector to hold the std::string objects, which will own the char* allocations. Then you can use another vector to hold the actual pointers. Something like this:

    const char * binaryPath = "/bin/foo";
    
    std::vector<std::string> argStrings;
    std::vector<char *> argPointers;
    
    std::string temp = "some text and stuff here";
    istringstream s(temp);
    
    // argv[0] should always contain the binary's path.
    argPointers.push_back(binaryPath);
    
    while (s >> temp) {
        argStrings.push_back(temp);
    
        std::cout << "TOKEN " << argStrings.size()
                  << " =" << argStrings.back() << std::endl;
    
        // We cast away the const as required by the exec() family of functions.
        argPointers.push_back(const_cast<char *>(argStrings.back().c_str()));
    }
    
    // exec() functions expect a NULL pointer to terminate the arguments array.
    argPointers.push_back(nullptr);
    
    // Now we can do our exec safely.
    execv(binaryPath, &argPointers[0]);
    

    In this case argStrings owns the actual string allocations, and we are using argPointers only to hold the array of pointers we will be passing to execv(). The const_cast is safe because execv() does not modify the strings. (The argument is char * const [] for compatibility with older C code; the functions behave as though the argument is const char * const [].)