Firstly, there's a memory leak possible, because strlist
performs two allocations using new[]
. If the first succeeds, but the second throws std::bad_alloc
, then we have no reference to *result
with which to delete[]
it.
The answer by Incomputable shows how the interface can be greatly improved.
I have a minor point that might be overlooked when reading that answer, and an efficiency improvement that can be incorporated into the original or into the proposed RAII object.
The minor point is that strlist()
requires a reference to a mutable vector for no good reason - the signature should be
char ** strlist(const std::vector<std::string> &input);
// ^^^^^
The efficiency improvement is that we know the total storage requirement for all the strings at the start of the function/constructor, so we can make a single allocation and place all our strings within that block instead of making separate allocations for each string to be accessed. See example code below.
From C++11 onwards, we could go further, and make our object be a view object, simply storing pointers to the data()
of the input strings (which would now need to be mutable - consider pass-by-value, and call it using std::move()
where that's useful).
Finally, is there a good reason that this should work only with std::vector
and not with other containers?
Single-allocation method
Here's how to make two passes over input
to save making several small allocations. I'm keeping (nearly) the original interface to make the changes more obvious, but I really recommend you create a type to ensure that the memory management is automatic.
#include <cstring>
#include <string>
#include <vector>
char *const *strlist(const std::vector<std::string>& input)
{
char **result = new char*[input.size() + 1];
std::size_t storage_size = 0;
for (auto const& s: input) {
storage_size += s.size() + 1;
}
try {
char *storage = new char[storage_size];
char *p = storage;
char **q = result;
for (auto const& s: input) {
*q++ = std::strcpy(p, s.c_str());
p += s.size() + 1;
}
*q = nullptr; // terminate the list
return result;
}
catch (...) {
delete[] result;
throw;
}
}
void del_strlist(char *const *strings)
{
// First string is the allocated storage
delete[] strings[0];
delete[] strings;
}
#include <iostream>
int main()
{
std::vector<std::string> args{ "/bin/ls", "ls", "-l" };
auto v = strlist(args);
for (auto p = v; *p; ++p) {
std::cout << '\'' << *p << "'\n";
}
del_strlist(v);
}
Single-allocation method, with smart pointer
We can use std::unique_ptr
to hold our data, if we don't mind using a custom deleter:
#include <cstring>
#include <memory>
#include <string>
#include <vector>
auto strlist(const std::vector<std::string>& input)
{
static auto const deleter = [](char**p) {
// First string is the allocated storage
delete[] p[0];
delete[]p;
};
std::unique_ptr<char*[], decltype(deleter)>
result{new char*[input.size() + 1], deleter};
// Ensure that destructor is safe (in case next 'new[]' fails)
result[0] = nullptr;
std::size_t storage_size = 0;
for (auto const& s: input) {
storage_size += s.size() + 1;
}
char *p = result[0] = new char[storage_size];
char **q = result.get();
for (auto const& s: input) {
*q++ = std::strcpy(p, s.c_str());
p += s.size() + 1;
}
*q = nullptr; // terminate the list
return result;
}
#include <iostream>
int main()
{
std::vector<std::string> args{ "/bin/ls", "ls", "-l" };
auto v = strlist(args);
for (auto p = v.get(); *p; ++p) {
std::cout << '\'' << *p << "'\n";
}
}
You should see that this is a bit simpler to write and to use than my first version.
P.S. Both demos compile with g++ -std=c++17 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Weffc++
and run under Valgrind with no warnings or errors, in case that's not already assumed.
del_strlist
returns a boolean? What is the result used for? \$\endgroup\$vector<string>
) rather than trying to maintain a parallelvector<string>
\$\endgroup\$char**
you pass? Do the pointers need to remain valid throughout, or can they be discarded after the call? \$\endgroup\$