0
votes

Let's say I have a class that wraps a string literal:

template <size_t N>
class literal {
public:
    constexpr literal(const char(&s)[N+1]) : wrapped_(s) {}

    constexpr const char * c_str() const { return wrapped_; }
    constexpr size_t size() const { return N; }

private:
    const char (&wrapped_)[N+1];
};

template <size_t N>
literal<N-1> make_literal(const char (&s)[N]) { return literal<N-1>(s); }

Now, I'd like for instances of this wrapped string type to be convertible back to a const char[N] implicitly, in a way I can still access its size. I'd like to be able to do something like:

template <size_t N>
void foo(const char(&s)[N]) {
    std::cout << N << ": " << s << std::endl;
}

int main() {
    constexpr auto s = make_literal("testing");
    foo(s);
}

My goal is to have one function defined for foo() that can accept actual string literals as well as wrapped literals. I've tried adding a user-defined conversion operator to the class definition:

using arr_t = char[N+1];    
constexpr operator const arr_t&() const { return wrapped_; }

But this gives me the following with clang:

candidate template ignored: could not match 'const char [N]' against 'const literal<7>'

If I change the call to foo() to the following, it works:

foo((const char(&)[8])s);

...which means that the conversion operator works, but not in the context of template argument deduction. Is there any way I can make this work without defining foo() specifically to take a wrapped literal?

1

1 Answers

0
votes

The problem you are having is templates never do conversions of the parameters. Since you give it a const literal<7>, that is all it has to work with.

The easy fix for this is to add an overload and then do the cast in the overload to call your string literal version. That should look like

template <size_t N>
void foo(const literal<N> &lit) {
    foo(static_cast<typename literal<N>::arr_t&>(lit)); // explicitly cast to the array type alias
}

which gives you a full example of

template <size_t N>
class literal {
public:
    constexpr literal(const char(&s)[N+1]) : wrapped_(s) {}

    constexpr const char * c_str() const { return wrapped_; }
    constexpr size_t size() const { return N; }
    using arr_t = const char[N+1];    // <- Add const here since literals are const char[N]
    constexpr operator const arr_t&() const { return wrapped_; }

private:
    const char (&wrapped_)[N+1];
};

template <size_t N>
constexpr literal<N-1> make_literal(const char (&s)[N]) { return literal<N-1>(s); }

template <size_t N>
void foo(const char(&s)[N]) {
    std::cout << N << ": " << s << std::endl;
}

template <size_t N>
void foo(const literal<N> &lit) {
    foo(static_cast<typename literal<N>::arr_t&>(lit)); // explicitly cast to the array type alias
}

int main() {
    constexpr auto s = make_literal("testing");
    foo(s);
}

Yes, you are adding an overload but all of the important code does not have to be duplicated.


If you can use C++17 and you don't mind a little indirection you could do all of this with one function using a std::string_view and providing literal with a operator std::string_view. That would look like

template <size_t N>
class literal {
public:
    constexpr literal(const char(&s)[N+1]) : wrapped_(s) {}

    constexpr const char * c_str() const { return wrapped_; }
    constexpr size_t size() const { return N; }
    using arr_t = const char[N+1];    
    constexpr operator std::string_view() const { return wrapped_; }

private:
    const char (&wrapped_)[N+1];
};

template <size_t N>
constexpr literal<N-1> make_literal(const char (&s)[N]) { return literal<N-1>(s); }

void foo(std::string_view s) {
    std::cout << s.size() << ": " << s << std::endl;
}


int main() {
    constexpr auto s = make_literal("testing");
    foo(s);
    foo("testing");
}