Reputation: 38941
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.
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));
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
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