par
par

Reputation: 17724

Is it possible in C++ to enforce a string-literal function argument?

I'm wondering if it's possible in C++ to declare a function parameter that must be a string literal? My goal is to receive an object that I can keep only the pointer to and know it won't be free()ed out from under me (i.e. has application lifetime scope).

For example, say I have something like:

#include <string.h>

struct Example {
    Example(const char *s) : string(s) { }

    const char *string;
};

void f() {
    char *freeableFoo = strdup("foo");

    Example e(freeableFoo);     // e.string's lifetime is unknown
    Example e1("literalFoo");   // e1.string is always valid

    free(freeableFoo);

    // e.string is now invalid
}

As shown in the example, when freeableFoo is free()ed the e.string member becomes invalid. This happens without Example's awareness.

Obviously we can get around this if Example copies the string in its constructor, but I'd like to not allocate memory for a copy.

Is a declaration possible for Example's constructor that says "you must pass a string literal" (enforced at compile-time) so Example knows it doesn't have to copy the string and it knows its string pointer will be valid for the application's lifetime?

Upvotes: 11

Views: 1999

Answers (2)

Maxim Egorushkin
Maxim Egorushkin

Reputation: 136435

if it's possible in C++ to declare a function parameter that must be a string literal?

In C++ string literals have type char const[N], so that you can declare a parameter to be of such a type:

struct Example {
    template<size_t N>
    Example(char const(&string_literal)[N]); // In C++20 declare this constructor consteval.

    template<size_t N>
    Example(char(&)[N]) = delete; // Reject non-const char[N] arguments.
};

However, not every char const[N] is a string literal. One can have local variables and data members of such types. In C++20 you can declare the constructor as consteval to make it reject non-literal arguments for string_literal parameter.


Conceptually, you'd like to determine the storage duration of the argument to a constructor/function parameter. Or, more precisely, whether the argument has a longer lifetime than Example::string reference to it. C++ doesn't provide that, C++20 consteval is still a poor-man's proxy for that.

gcc extension __builtin_constant_p detetmines whether an expression is a compile-time constant which is widely used in preprocessor macros in Linux kernel source code. However, it can only evaluate to 1 on expressions, but never on functions' parameters, so that its use is limited to preprocessor macros.


The traditional solution for the problem of different object lifetimes has been organizing objects into a hierarchy, where objects at lower levels have smaller lifetimes than objects at higher levels, and hence, an object can always have a plain pointer to an object at a higher level of hierarchy valid. This approach is somewhat advanced, labour intensive and error prone, but it totally obviates the need for any garbage collection or smart-pointers, so that it's only used in ultra critical applications where no cost is too high. The opposite extreme of this approach is using std::shared_ptr/std::weak_ptr for everything which snowballs into maintenance nightmare pretty rapidly.

Upvotes: 1

BeeOnRope
BeeOnRope

Reputation: 64996

In C++20 you can do it using a wrapper class that has a consteval converting constructor which takes a string literal:

struct literal_wrapper
{
    template<class T, std::size_t N, std::enable_if_t<std::is_same_v<T, const char>>...>
    consteval literal_wrapper(T (&s)[N]) : p(s) {}

    char const* p;
};

The idea is that string literals have type const char[N] and we match this.

Then you can use this wrapper class in places where want to enforce passing a string literal:

void takes_literal(string_literal lit) {
  // use lit.p here
}

You can call this as foo("foobar").

Note that this will also match static-storage const char[] arrays, like so:

const char array[] = {'a'};


takes_literal(array); // this compiles

Static arrays have most of the same characteristics as string literals, however, e.g., indefinite storage duration, which may work for you.

It does not match local arrays because the decayed pointer value is not a constant expression (that's where consteval comes in).

This answer is almost directly copied from the first variant suggested in C.M.'s comment on the question.

Upvotes: 6

Related Questions