简体   繁体   中英

C++ How can I improve this bit of template meta-program to give back the array including the size?

I've got a utility called choose_literal which chooses a literal string encoded as char*, wchar_*, char8_t*, char16_t*, char32_t* depending on the desired type (the choice).

It looks like this:

    template <typename T>
    constexpr auto choose_literal(const char * psz, const wchar_t * wsz, const CHAR8_T * u8z, const char16_t * u16z, const char32_t * u32z) {
        if constexpr (std::is_same_v<T, char>)
            return psz;
        if constexpr (std::is_same_v<T, wchar_t>)
            return wsz;
    #ifdef char8_t
        if constexpr (std::is_same_v<T, char8_t>)
            return u8z;
    #endif
        if constexpr (std::is_same_v<T, char16_t>)
            return u16z;
        if constexpr (std::is_same_v<T, char32_t>)
            return u32z;
    }

I supply a little preprocessor macro to make this work w/o having to type each of those string encodings manually:

// generates the appropriate character literal using preprocessor voodoo
// usage: LITERAL(char-type, "literal text")
#define LITERAL(T,x) details::choose_literal<T>(x, L##x, u8##x, u##x, U##x)

This of course only works for literal strings which can be encoded in the target format by the compiler - but something like an empty string can be, as can ASCII characters (ie az, 0-9, etc., which have representations in all of those encodings).

eg here's a trivial bit of code that will return the correct empty-string given a valid character type 'T':

template <typename T>
constexpr const T * GetBlank() {
    return LITERAL(T, "");
}

This is great as far as it goes, and it works well enough in my code.

What I'd like to do is to refactor this such that I get back the character-array including its size, as if I'd written something like:

const char blank[] = "";

or

const wchar_t blank[] = L"";

Which allows the compiler to know the length of the string-literal, not just its address.

My choose_literal<T>(str) returns only the const T * rather than the const T (&)[size] which would be ideal.

In general I'd love to be able to pass such entities around intact - rather than have them devolve into just a pointer.

But in this specific case, is there a technique you might point me towards that allows me to declare a struct with a data-member for the desired encoding which then also knows its array-length?

A little bit of constexpr recursion magic allows you to return a string_view of the appropriate type.

#include <string_view>
#include <type_traits>
#include <iostream>

template <typename T, class Choice, std::size_t N, class...Rest>
constexpr auto choose_literal(Choice(& choice)[N], Rest&...rest)
{
    using const_char_type = Choice;

    using char_type = std::remove_const_t<const_char_type>;

    if constexpr (std::is_same_v<T, char_type>)
    {
        constexpr auto extent = N;
        return std::basic_string_view<char_type>(choice, extent - 1);
    }
    else
    {
        return choose_literal<T>(rest...);
    }
}

int main()
{
    auto clit = choose_literal<char>("hello", L"hello");
    std::cout << clit;

    auto wclit = choose_literal<wchar_t>("hello", L"hello");
    std::wcout << wclit;
}

https://godbolt.org/z/4roZ_O

If it were me, I'd probably want to wrap this and other functions into a constexpr class which offers common services like printing the literal in the correct form depending on the stream type, and creating the correct kind of string from the literal.

For example:

#include <string_view>
#include <type_traits>
#include <iostream>
#include <tuple>

template <typename T, class Choice, std::size_t N, class...Rest>
constexpr auto choose_literal(Choice(& choice)[N], Rest&...rest)
{
    using const_char_type = Choice;

    using char_type = std::remove_const_t<const_char_type>;

    if constexpr (std::is_same_v<T, char_type>)
    {
        constexpr auto extent = N;
        return std::basic_string_view<char_type>(choice, extent - 1);
    }
    else
    {
        return choose_literal<T>(rest...);
    }
}

template<class...Choices>
struct literal_chooser
{
    constexpr literal_chooser(Choices&...choices)
    : choices_(choices...)
    {}

    template<class T>
    constexpr auto choose() 
    {
        auto invoker = [](auto&...choices)
        {
            return choose_literal<T>(choices...);
        }; 

        return std::apply(invoker, choices_);
    }

    std::tuple<Choices&...> choices_;
};

template<class Char, class...Choices>
std::basic_ostream<Char>& operator<<(std::basic_ostream<Char>& os, literal_chooser<Choices...> chooser)
{
    return os << chooser.template choose<Char>();
}

template<class Char, class...Choices>
std::basic_string<Char> to_string(literal_chooser<Choices...> chooser)
{
    auto sview = chooser.template choose<Char>();
    return std::basic_string<Char>(sview.data(), sview.size());
}


int main()
{
    auto lit = literal_chooser("hello", L"hello");

    std::cout << lit << std::endl;
    std::wcout << lit << std::endl;

    auto s1 = to_string<char>(lit);
    auto s2 = to_string<wchar_t>(lit);

    std::cout << s1 << std::endl;
    std::wcout << s2 << std::endl;   
}

The use of the reference argument type Choices& is important. C++ string literals are references to arrays of const Char . Passing by value would result in the literal being decayed into a pointer, which would lose information about the extent of the array.

we can add other services, written in terms of the literal_chooser:

template<class Char, class...Choices>
constexpr std::size_t size(literal_chooser<Choices...> chooser)
{
    auto sview = chooser.template choose<Char>();
    return sview.size();
}

We're going to change the function so that it takes a const T (&)[size] for each input, and the return type is going to be decltype(auto) . Using decltype(auto) prevents the return from decaying into a value, preserving things like references to arrays.

Updated function:

template <typename T, size_t N1, size_t N2, size_t N3, size_t N4>
constexpr decltype(auto) choose_literal(const char (&psz)[N1], const wchar_t (&wsz)[N2], const char16_t (&u16z)[N3], const char32_t (&u32z)[N4]) {
    if constexpr (std::is_same<T, char>())
        return psz;
    if constexpr (std::is_same<T, wchar_t>())
        return wsz;
    if constexpr (std::is_same<T, char16_t>())
        return u16z;
    if constexpr (std::is_same<T, char32_t>())
        return u32z;
}

In main, we can assign the result to something of type auto&& :

#define LITERAL(T,x) choose_literal<T>(x, L##x,  u##x, U##x)

int main() {
    constexpr auto&& literal = LITERAL(char, "hello");  
    return sizeof(literal); // Returns 6
}

Potential simplification

We can simplify the choose_literal function by making it recursive, that way it can be expanded for any number of types. This works without any changes to the LITERAL macro.

template<class T, class Char, size_t N, class... Rest>
constexpr decltype(auto) choose_literal(const Char(&result)[N], Rest const&... rest) {
    if constexpr(std::is_same_v<T, Char>)
        return result; 
    else
        return choose_literal<T>(rest...);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM