Using template to handle string and wstring
Asked Answered
A

4

5

I have following two functions:

void bar(const std::string &s)
{
    someCFunctionU(s.c_str());
}

void bar(const std::wstring &s)
{
    someCFunctionW(s.c_str());
}

Both of these call some C function which accepts const char * or const wchar_t * and have U or W suffixes respectively. I would like to create a template function to handle both of these cases. I tried following attempt:

template <typename T>
void foo(const std::basic_string<T> &s)
{
    if constexpr (std::is_same_v<T, char>)
        someCFunctionU(s.c_str());
    else
        someCFunctionW(s.c_str());
}

But this does not seem to work correctly. If I call:

foo("abc");

this will not compile. Why is that? why a compiler is not able to deduce the proper type T to char? Is it possible to create one function which would handle both std::string and std::wstring?

Anti answered 20/11, 2018 at 18:53 Comment(5)
"abc" is const char* and not a std::(w)stringShush
It's actually a const char[N], but it will decay to a const char*Dorren
@Dorren Why would it decay? During deduction, the parameter is a reference. And during constructor call, it will use the the overload which takes std::initializer_list<char>.Credits
@Credits I didn't mean during deduction. I just mean generally it will decay to a char*. I should have been more specific.Dorren
@Dorren Yes, sometimes. But not in this context. I don't know which part you mean. Problem is that such decay won't happen here in any part.Credits
P
6

this will not compile. Why is that? why a compiler is not able to deduce the proper type T to char?

As better explained by others, "abc" is a char[4], so is convertible to a std::basic_string<char> but isn't a std::basic_string<char>, so can't be deduced the T type as char for a template function that accept a std::basic_string<T>.

Is it possible to create one function which would handle both std::string and std::wstring?

Yes, it's possible; but what's wrong with your two-function-in-overloading solution?

Anyway, if you really want a single function and if you accept to write a lot of casuistry, I suppose you can write something as follows

template <typename T>
void foo (T const & s)
{
    if constexpr ( std::is_same_v<T, std::string> )
        someCFunctionU(s.c_str());                       
    else if constexpr ( std::is_convertible_v<T, char const *>)
        someCFunctionU(s);
    else if constexpr ( std::is_same_v<T, std::wstring> )
        someCFunctionW(s.c_str());
    else if constexpr ( std::is_convertible_v<T, wchar_t const *> )
        someCFunctionW(s);
    // else exception ?
}

or, a little more synthetic but less efficient

template <typename T>
void foo (T const & s)
{
    if constexpr ( std::is_convertible_v<T, std::string> )
        someCFunctionU(std::string{s}.c_str());
    else if constexpr (std::is_convertible_v<T, std::wstring> )
        someCFunctionW(std::wstring{s}.c_str());
    // else exception ?
}

So you should be able to call foo() with std::string, std::wstring, char *, wchar_t *, char[] or wchar_t[].

Pearle answered 20/11, 2018 at 19:27 Comment(0)
D
4

The issue here is that in foo("abc");, "abc" is not a std::string or a std::wstring, it is a const char[N]. Since it isn't a std::string or a std::wstring the compiler cannot deduce what T should be and it fails to compile. The easiest solution is to use what you already have. The overloads will be considered and it is a better match to convert "abc" to a std::string so it will call that version of the function.

If you want you could use a std::string_view/std::wstring_view instead of std::string/std::wstring so you don't actually allocate any memory if you pass the function a string literal. That would change the overloads to

void bar(std::string_view s)
{
    someCFunctionU(s.data());
}

void bar(std::wstring_view s)
{
    someCFunctionW(s.data());
}

Do note that std::basic_string_view can be constructed without having a null terminator so it is possible to pass a std::basic_string_view that won't fulfill the null terminated c-string requirement that your C function has. In that case the code has undefined behavior.

Dorren answered 20/11, 2018 at 19:2 Comment(4)
Hmm. string_view can't guarantee a null-terminated string, so passing s.data() to the C-style function is dangerous and should probably be avoided, since that C-style function expects a null-terminated string. If the C-style function could be changed to take a pointer and a length, all would be fine. Alternatively, there may be some non-standard string_view implementations which guarantee null-termination, meaning that they can only be at the end of a stringBangka
@Bangka That really upsets me with string_view that we can't do this. They give me this nice shinny thing but then rip the carpet out from under my feet. I'll put a warning in the answer for the OP, just so they know, but it should be okay if the only feed strings/c-strings to the function.Dorren
string_view with guaranteed null-termination is not as useful. It means that the string_view can only point to the end of strings, rather than any substring.Bangka
@Bangka That's why I really wanted a slice class. I always envisioned a string_view as a read only view of a string with all the member functions. A slice or string_slice would have the non-guarantees.Dorren
C
2

Yes, there exist a type, i.e. std::basic_string<char>, which can be copy initialized from expression "abc". So you can call a function like void foo(std::basic_string<char>) with argument "abc".

And no, you can't call a function template template <class T> void foo(const std::basic_string<T> &s) with argument "abc". Because in order to figure out whether the parameter can be initialized by the argument, the compiler need to determine the template parameter T first. It will try to match const std::basic_string<T> & against const char [4]. And it will fail.

The reason why it will fail is because of the template argument deduction rule. The actual rule is very complicated. But in this case, for std::basic_string<char> to be examined during the deduction, compiler will need to look for a proper "converting constructor", i.e. the constructor which can be called implicitly with argument "abc", and such lookup isn't allowed by the standard during deduction.

Yes, it is possible to handle std::string and std::wstring in one function template:

void foo_impl(const std::string &) {}
void foo_impl(const std::wstring &) {}

template <class T>
auto foo(T &&t) {
    return foo_impl(std::forward<T>(t));
}
Credits answered 20/11, 2018 at 19:5 Comment(2)
std::basic_string<char> is exactly the same as std::stringBangka
@Bangka Yes, std::string is its alias. But the original question is using std::basic_string. And in this context, using std::string is inconvenient.Credits
M
2

A workaround in C++17 is:

template <typename T>
void foo(const T &s)
{
    std::basic_string_view sv{s}; // Class template argument deduction

    if constexpr (std::is_same_v<typename decltype(sv)::value_type, char>)
        someCFunctionU(sv.data());
    else
        someCFunctionW(sv.data());
}

And to avoid issue mentioned by Justin about non-null-terminated string

template <typename T> struct is_basic_string_view : std::false_type {};

template <typename T> struct is_basic_string_view<basic_string_view<T>> : std::true_type
{};

template <typename T>
std::enable_if_t<!is_basic_string_view<T>::value> foo(const T &s)
{
    std::basic_string_view sv{s}; // Class template argument deduction

    if constexpr (std::is_same_v<typename decltype(sv)::value_type, char>)
        someCFunctionU(sv.data());
    else
        someCFunctionW(sv.data());
}
Malka answered 20/11, 2018 at 19:13 Comment(2)
This has the same danger I mentioned in NathanOliver's answer: if you pass a non-null-terminated string_view to foo(...), this will accept it, passing a non-null-terminated string to the C functions, which is likely to cause problems.Bangka
@Justin: Safer version added.Malka

© 2022 - 2024 — McMap. All rights reserved.