Create a std::string from char[] with strndup like semantics

Viewed 441

I have a

char txt_msg[80];

The array can contain up to 80 characters, e.g. there is no guarantee that there is a terminating null. If there are less than 80 characters however, there is a terminating null.

Right now I'm using this to get a std::string from this:

std::string(txt_msg, txt_msg + ::strnlen(txt_msg, sizeof(txt_msg)));

to create a C++ string, which looks kind of offensive. Is there a more C++y way to do that?

3 Answers

I would probably have done something like this:

char txt_msg[80];

auto s = std::string(std::begin(txt_msg), std::find(std::begin(txt_msg), std::end(txt_msg), '\0'));

std::find will return the position of either the first null terminator or the end of the array.

Is there a more C++y way to do that?

As far as the construction of the std::string is concerned, not really. Though, at the very least, since you already know the max length of the char[], you can use the std::string(const char*, size_type) constructor instead of the std::string(InputIt, InputIt) constructor, thus the constructor can avoid having to calculate the length:

std::string(txt_msg, ::strnlen(txt_msg, sizeof(txt_msg));

Since strnlen() is a non-standard POSIX extension, it would not be hard to write a manual implementation, if needed:

#include <algorithm>

size_t strnlen(const char *s, size_t maxlen)
{
    const char *s_end = s + maxlen;
    const char *found = std::find(s, s_end, '\0');
    return (found != s_end) ? size_t(found - s) : maxlen;
}

That being said, a C++ solution to your problem would be to wrap the std::string construction in a helper template function, eg:

template<size_t N>
std::string to_string(const char (&arr)[N])
{
    return std::string(arr, strnlen(arr, N));
}

And then you can do this when needed:

char txt_msg[80];
...
std::string s = to_string(txt_msg);

Rather than doing this:

char txt_msg[80];
...
std::string s = std::string(txt_msg, txt_msg + strnlen(txt_msg, sizeof(txt_msg)));
//or
std::string s = std::string(txt_msg, strnlen(txt_msg, sizeof(txt_msg)));

Perhaps you should consider using an std::string_view - a non-owning string-like reference-type which can be used mostly like std::string; in your case, it would be backed by your message array:

auto sv = std::string_view{txt_msg, ::strnlen(txt_msg, std::extent_v<decltype(txt_msg)>};

But this is, still, indeed, quite iffy, and breaks the DRY principle badly: 3 repetitions. So, how about we write a little utility function? :

inline std::string_view 
constrain_by_nul(std::string_view sv) {
    return sv.substr(0, sv.find('\0'));
}

with this, you could write:

auto sv = constrain_by_nul(std::string_view{txt_msg, std::size(txt_msg)});

Better, but not quite there yet: We mention txt_msg twice. Unfortunately, we can't construct a string view directly from a container (IIANM). So maybe another utility function?

template<typename CharT, std::size_t N> 
std::basic_string_view<CharT>
inline make_string_view(CharT (&arr)[N]) { 
    return {arr, N};
};

Now you can write:

auto sv = constrain_by_nul(make_string_view(txt_msg));

And that's pretty much what you wanted to do in the first place. With decent compiler optimization it might actually compile to the same thing. And - no copying and no heap allocation, since it's not an std::string.


Read more about string views in this SO question: What is string_view?

Related