I want to use a C-style API from C++ which takes a variable number of structs with char* members like this:
typedef struct _FOO
{
const char * name;
const char * value;
} FOO;
void ApiFunc(FOO const* args, unsigned count);
To fill the parameters, I need to loop over some other data and create FOO entries on the fly. What would be the most elegant way to do that?
The following approach seems simple at first, but does not work (since the string instances go out of scope and are destroyed before the call to ApiFunc()):
// Approach A: this does *not* work
std::vector<FOO> args;
for (...)
{
string name = ... // something which gets
string value = ... // calculated in the loop
args.push_back( FOO{name.c_str(), value.c_str()} );
}
ApiFunc( args.data(), args.size() );
Putting the string objects in a vector (to prevent them from being destroyed) doesn't work either - as the strings are copied when put into the vector, and the original ones are still destroyed:
// Approach B: this also does *not* work
std::vector<string> strings;
for (...)
{
string name = ... // something which gets
string value = ... // calculated in the loop
strings.push_back( name );
strings.push_back( value );
args.push_back( FOO{name.c_str(), value.c_str()} );
}
ApiFunc( args.data(), args.size() );
I can prevent that by creating the string objects on the heap and using auto_ptr to keep track of them, but is there a better way?
// Approach C: this does work
std::vector<auto_ptr<string>> strings;
for (...)
{
string* name = new ...
string* value = new ...
strings.push_back( auto_ptr<string>(name) );
strings.push_back( value );
args.push_back( FOO{name.c_str(), value.c_str()} );
}
ApiFunc( args.data(), args.size() );
While approach C. seems to work, I find it rather unobvious / hard to understand. Any suggestions how I could improve it?
I am pretty sure that std::vector<auto_ptr<T>> is not permitted by the standard. Use unique_ptr instead. Alternatively, build the strings in the vector, and then build args from that.
std::vector<std::pair<std::string, std::string>> strings;
for (...)
{
const std::string name = ...;
const std::string value = ...;
strings.push_back( std::make_pair( name, value ) );
}
// Note: This loop must be separate so that 'strings' will not reallocate and potentially
// invalidate the pointers returned by elements in it.
for (const auto& pair : strings)
{
args.push_back( FOO{pair.first.c_str(), pair.second.c_str()} );
}
ApiFunc( args.data(), args.size() );
Since you're interfacing with a C API, you're going to have to do things the C API way. That is, allocate heap buffers and deallocate them afterwards. This is actually a prime example of the utility of unique_ptr<T[]>:
//Helper function
std::unique_ptr<char[]> cpp_strdup(const std::string &str)
{
std::unique_ptr<char[]> ret = new char[str.size() + 1];
//Must copy the NUL terminator too.
std::copy(str.data(), str.data() + str.size() + 1, ret.get());
return ret;
}
//In your function:
std::vector<unique_ptr<char[]>> strings;
for (...)
{
string name = ... // something which gets
strings.push_back(cpp_strdup(name));
string value = ... // calculated in the loop
strings.push_back(cpp_strdup(value));
args.push_back( FOO{strings[strings.size() - 2].get(), strings[strings.size() - 1].get()} );
}
By using unique_ptr instead of std::string in the strings vector, you neatly avoid reallocating the strings themselves when strings undergoes reallocation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With