2

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?

jpo234
  • 419
  • 3
  • 9
  • Will the function that fills the array "return" the number of characters it filled? Or "return" some other indicator if the null-terminator was added or not? – Some programmer dude May 14 '21 at 16:49
  • 3
    You could do `std::string(txt_msg, ::strnlen(txt_msg, sizeof txt_msg));` using the `(char*, size)` constructor instead of the the `(begin, end)` constructor. Hmmm, apparently `strnlen` isn't C or C++ standard (it's POSIX.1-2008), but wouldn't be hard to write. – Eljay May 14 '21 at 16:49
  • @Someprogrammerdude No, this is inside a structure that gets filled by a C library. I only get the filled structure. – jpo234 May 14 '21 at 16:52
  • 2
    `which looks kind of offensive` I do not feel offended by it, looks fine. – KamilCuk May 14 '21 at 16:55
  • Assuming the correct `size` is passed into the constructor, then then constructor will have the desired behavior. But if an incorrect `size` is passed in... GIGO. – Eljay May 14 '21 at 16:55
  • @KamilCuk The "offensive" parts are: 1) it relies on a C library function 2) strnlen is not even standardized C/C++ (it's POSIX). – jpo234 May 14 '21 at 16:57
  • @jpo234 "*this is inside a structure that gets filled by a C library*" - why isn't the C library telling you what the length of the array is? That is not very C-like for a buffer that is not guaranteed to be null-terminated. – Remy Lebeau May 14 '21 at 17:10
  • @RemyLebeau C code that deals with this structure uses strncpy. As for why: No idea. I did not write the C library. – jpo234 May 14 '21 at 17:15
  • How about: `char temp[81] = {0,}; memcpy(temp, txt_msg, 80);` and then construct your `std:string` from the `temp` array. Or is that too silly? – Adrian Mole May 14 '21 at 17:16
  • @AdrianMole that will "work", but it is really no better than just using `strnlen()` if it is available. If you don't pass in a length, `std::string` will calculate a length ala `strlen()`. – Remy Lebeau May 14 '21 at 17:21
  • 1
    @Remy I agree it's no more efficient than using `strnlen` ... but its 'better' in the sense that it doesn't rely on a non-standard function being available. – Adrian Mole May 14 '21 at 17:26
  • @AdrianMole: TBH that's super ugly - both in explicitly defining a temporary and in using a magic number three times... you can avoid `strnlen()` differently - using the `std::string_view` methods (see my answer, hint hint). – einpoklum May 16 '21 at 19:01

3 Answers3

2

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.

Galik
  • 47,303
  • 4
  • 80
  • 117
0

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)));
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • @einpoklum `std::to_string()` only supports numeric types, not character arrays. And there is no `sizeof()` in my "final suggestion". What DRY violation are you referring to? – Remy Lebeau May 16 '21 at 18:58
  • Ok, so - I would be hesitant to have someone write a `to_string()` which does something else. Perhaps a different name for the function? – einpoklum May 16 '21 at 18:59
  • @einpoklum name it whatever you want. I prefer using using something more consistent with the standard – Remy Lebeau May 16 '21 at 19:02
  • @einpoklum read my answer again more carefully. You are referring to code that I said NOT to use, which is related to the OP's original code. – Remy Lebeau May 16 '21 at 20:22
  • Oh yes, I'm sorry, I jumped a line. Another point is, that you force the user of `to_string()` to guess whether the string length will be the array size always, or sometimes less than that. It's not obvious. And the choice of name doesn't make it obvious either. Finally, you're still using a POSIX-only function in the implementation of `to_string()` while you have nice enough standard library facilities to use instead. – einpoklum May 16 '21 at 20:39
  • @einpoklum this code is not forcing the user to guess anything. Clearly you missed what the OP is asking for. If you don't like the name, use whatever you want. And I'm not using the POSIX function, did you miss the entire 1st half of my answer? – Remy Lebeau May 16 '21 at 20:56
  • Remy Lebau: You do suggest that, but to actually use that custom implementation you would need to move out of the default namespace, where strnlen exists. Either that or use some `#ifdef`'ing etc. – einpoklum May 16 '21 at 21:56
0

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?

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • `std::extent_v` can be replaced with `std::size(txt_msg)` instead. And in light of your want to avoid DRY, `make_string_view()` can use `arr` as-is instead of `std::begin(arr)` and `N` instead of `std::end(arr)`: `return std::string_view{arr, N};` – Remy Lebeau May 16 '21 at 21:04
  • I wasn't aware of `std::size()`, thanks! And good point about the string_view construction. I've improved it even further... – einpoklum May 16 '21 at 21:49