Search code examples
c++arraysternarychar-pointervariable-initialization

Can I Initialize a char[] with a Ternary?


I asked a question about it and didn't get a really clear answer, but after reading this article I started preferring const char[] to const char*.

I've come upon a difficulty when initializing with a ternary. Given const bool bar, I've tried:

  1. const char foo[] = bar ? "lorem" : "ipsum" which gives me the error:

error: initializer fails to determine size of foo

  1. const char foo[] = bar ? { 'l', 'o', 'r', 'e', 'm', '\0' } : { 'i', 'p', 's', 'u', 'm', '\0' } which gives me the error:

error: expected primary-expression before { token

Is there a way to initialize a const char [] with a ternary, or do I have to switch to const char* here?


Solution

  • Since string literals are lvalues, you can take const references of them, which can be used in a ternary.

    // You need to manually specify the size
    const char (&foo)[6] = bar ? "lorem" : "ipsum";
    
    // Or (In C++11)
    auto foo = bar ? "lorem" : "ipsum";
    

    auto would behave exactly the same (Except that you would have to specify the size).

    If you want to do it for different length strings, unfortunately "bool ? const char[x] : const char[y]" would only be an array type if they have the same size (Otherwise they would both decay into pointers, and the expression would be of type const char*). To remedy this, you would have to manually pad the string with \0 characters (And now you can't do sizeof(foo) - 1 to get the size, you would have to do strlen(foo)).

    For example, instead of:

    auto foo = bar ? "abc" : "defg";  // foo is a const char*
    

    You would have to do:

    auto foo = bar ? "abc\0" : "defg"; // foo is const char(&)[5]
    // Note that if `bar` is true, foo is `{'a', 'b', 'c', '\0', '\0'}`
    

    Before you have to do this, consider that if you set your variables as const char * const, your compiler will more than likely optimise them to be exactly the same as if they were const char[], and probably also the same if they were only const char * (no const at the end) if you don't change the value.

    To not accidentally get a pointer, and fail fast if you do, I would use a helper function:

    #include <cstddef>
    
    template<std::size_t size, std::size_t other_size = size>
    constexpr auto conditional(bool condition, const char(&true_case)[size], const char(&false_case)[other_size]) noexcept -> const char(&)[size] {
        static_assert(size == other_size, "Cannot have a c-string conditional with c-strings of different sizes");
        return condition ? true_case : false_case;
    }
    
    // Usage:
    auto foo = conditional(bar, "lorem", "ipsum");
    

    If bar is a compile time constant, you can change the type of foo depending on the value of bar. For example:

    #include <cstddef>
    
    template<bool condition, std::size_t true_size, std::size_t false_size>
    constexpr auto conditional(const char(&true_case)[true_size], const char(&false_case)[false_size]) -> typename std::enable_if<condition, const char(&)[true_size]>::type {
        return true_case;
    }
    
    template<bool condition, std::size_t true_size, std::size_t false_size>
    constexpr auto conditional(const char(&true_case)[true_size], const char(&false_case)[false_size]) -> typename std::enable_if<!condition, const char(&)[false_size]>::type {
        return false_case;
    }
    
    // Or with C++17 constexpr if
    
    template<bool condition, std::size_t true_size, std::size_t false_size>
    constexpr auto conditional(const char(&true_case)[true_size], const char(&false_case)[false_size]) -> const char(&)[condition ? true_size : false_size] {
        if constexpr (condition) {
            return true_case;
        } else {
            return false_case;
        }
    }
    
    // Usage:
    auto foo = conditional<bar>("dolor", "sit");