Martin Ba
Martin Ba

Reputation: 38941

r-value reference to default argument temporary to hold the buffer for converting a wstring?

Background

I have code with a logging interface that takes const char* only.

My code deals with a mixed bag of std::string and std::wstring coming from different sources.

And, yes, I know that this is far from ideal, but let's focus on the technicalities.

Problem

I would like to feed both the std::string and std::wstring to a uniform conversion function, let's call it tc_str(T StrT) to ease construction of logging expressions.

For string, the implementation is straightforward:

const char* tc_str(const std::string& str) {
    return str.c_str();
}

For wstring, I have come up with the following solution that works on VS2022 afaict:

const char* tc_str(const std::wstring& wstr, std::string&& rTmpLifeExt = std::string()) {
    rTmpLifeExt = wchar_to_narrow(wstr);
    return rTmpLifeExt.c_str();
}

Here's the full test code Gist.

Quick usage snippet:

std::string narrow = ...
std::wstring wide = ...

printf("n: %s / w: %s", tc_str(narrow), tc_str(wide));

Question

Is this legal C++20?

According to https://stackoverflow.com/a/12554621/321013 (ref cppref) (ref: §12.2 [class.temporary]; §1.9 [intro.execution]) the reference lifetime should be extended "correctly" and no dangling pointers should occur:

... special case in §12.2 [class.temporary]:

p4 ... contexts in which temporaries are destroyed at a different point [...]

p5 ... when a reference is bound to a temporary. ...:

  • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

... in §1.9 [intro.execution] p11:

[ Note: ... subexpressions involved in evaluating default arguments (8.3.6) are considered to be created in the expression that calls the function, not the expression that defines the default argument. —end note ]

My biggest doubt would be with r-value vs. l-value references here, but it seems they are treated the same.

Given the constraint of that crappy logging API, is this a "good enough" solution or are there any major problems with it?

Upvotes: 1

Views: 92

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

Might I suggest a solution with less magic?

struct string_c_helper {
  std::string storage;
  char const* string_ptr = "";
  operator char const*() const { return string_ptr; }
  bool uses_storage() const { return string_ptr == storage.c_str(); }

  string_c_helper() {} // empty string

  string_c_helper( char const* ptr ):string_ptr(ptr) {}
  string_c_helper( std::string const& str):string_ptr(str.c_str()) {}

  struct store_string_t {};
  string_c_helper( store_string_t, std::string str ):
    storage(std::move(str)), string_ptr(storage.c_str())
  {}

  // use storage for wstrings:
  string_c_helper( std::wstring const& wstr):
    string_c_helper(store_string_t{}, (wchar_to_narrow(wstr))
  {}

  // allow move construction.  If it uses storage, copy the
  // storage, otherwise just copy the pointer:
  string_c_helper(string_c_helper&& o):
    string_c_helper(
      o.uses_storage()?
        string_c_helper(store_string_t{}, std::move(o.storage))
      :
        string_c_helper(o.string_ptr)
    )
  {}

  string_c_helper(string_c_helper const&)=delete;
  string_c_helper& operator=(string_c_helper const&)& = delete;
  string_c_helper& operator=(string_c_helper&& o)& = delete;
};
    

this type is a char const* and optionally a std::string to carry it. It can be implicitly cast to char const* regardless. It supports empty strings.

string_c_helper tc_str(const std::wstring& wstr) {
  return wstr;
}
string_c_helper tc_str(const std::string& str) {
  return str;
}

you can pass these to C-api objects. You can even return them from functions. Only once converted to a char const*, the validity ends at the end of the full-expression the conversion occurs at.

The same is basically true of yours, but the issue with yours is that working out why it works will confuse most C++ developers. So when it breaks, they'll have little hope of understanding what went wrong.

This basically "manually" does the string lifetime extension instead of having it occur magically.

(Aside: string_c_helper(string_c_helper&&) isn't infinite recursion because of guaranteed elision.)

Upvotes: 1

Related Questions