Constant evaluation in replacements

In a replacement, bronto::eval(expr) constant-evaluates expr and writes the result into the rewritten source. For example, bronto::eval(1 + 2) writes the value 3 rather than the spliced text 1 + 2. Captured expressions can appear inside expr, so a replacement can compute a new constant from the values matched at each site.

Example
constexpr int Square(int n) { return n * n; }

struct SquareTheArgument : bronto::rewrite_expr {
  BRONTO_BEFORE()
  void find(int n) { foo(n); }

  BRONTO_AFTER()
  void repl(int n) { bar(bronto::eval(Square(n))); }
};

With this rewrite, the call foo(3) becomes bar(9). Without bronto::eval it would instead become bar(Square(3)).

bronto::eval

Requirements

bronto::eval may only appear in a function annotated with BRONTO_AFTER(). Using it in a function annotated with BRONTO_BEFORE() is an error.

bronto::eval takes either one argument or two. The one-argument form, described here, writes the value as a plain literal. The two-argument form writes it as a user-defined literal instead.

After the captured expressions are substituted for the parameters they bind to, the first argument must be a constant expression. If the substitution fails, or the substituted expression cannot be constant-evaluated, the matched site is left unchanged and a diagnostic explains why.

Semantics

The argument is constant-evaluated and the resulting value is written into the replacement as a literal denoting the value exactly. The type of the expression determines the kind of literal that is generated.

Expression typeGenerated literalExample
boolboolean literaltrue
intinteger literal, no suffix42
unsignedinteger literal, u suffix42u
longinteger literal, l suffix42l
unsigned longinteger literal, ul suffix42ul
long longinteger literal, ll suffix42ll
unsigned long longinteger literal, ull suffix42ull
signed char, unsigned char, short, unsigned shortstatic_cast of an integer literalstatic_cast<short>(1000)
charcharacter literal, no prefix'a'
wchar_tcharacter literal, L prefixL'a'
char8_tcharacter literal, u8 prefixu8'a'
char16_tcharacter literal, u prefixu'a'
char32_tcharacter literal, U prefixU'a'
floatfloating-point literal, f suffix1.5f
doublefloating-point literal, no suffix1.5
long doublefloating-point literal, L suffix1.5L
string-like (see below)string literal, prefix matching the code unit"abc", L"abc"
character pointer (see below)string literal, prefix matching the code unit"abc", L"abc"

Character results

Printable ASCII characters are written verbatim, mnemonic escapes such as \n are used where they exist portably, and any other value uses a numeric escape. Note that signed char and unsigned char are treated as integer types, not character types.

Floating-point results

The expression is constant-evaluated with IEEE 754 arithmetic in the result type's format on the target described by your compile_commands.json. For example, the format of long double varies by target. The following applies:

  • Values are rounded to nearest with ties to even.
  • Operations are never contracted. For example, a * b + c is evaluated as a rounded multiply followed by a rounded add, never as a fused multiply-add, so the result may differ from what the same expression would compute at runtime under flags like -ffp-contract=fast.
  • The emitted literal denotes the evaluated value exactly. Any compiler that parses floating-point literals with correct rounding reproduces the value bit for bit. It is not necessarily the shortest spelling of that value.

String results

To output a string literal, the evaluated expression must be of a type that has a constexpr data() member function returning a pointer to a standard character type and a constexpr size() member function returning its length. std::string_view satisfies these requirements in C++17, and std::string does as well in C++20. The literal denotes each code unit of the result exactly, using the same escaping rules as character literals.

An expression whose type is itself a pointer to a standard character type, such as char const*, is also accepted. It is read as a NUL-terminated string. The pointer must point to a NUL-terminated constant, so a null pointer or an array without a terminator fails to constant-evaluate.

String Example
constexpr std::string KebabCase(std::string_view s) {
  std::string result;
  for (char c : s) { result.push_back(c == '_' ? '-' : c); }
  return result;
}

struct MigrateOptionNames : bronto::rewrite_expr {
  BRONTO_BEFORE()
  void find(char const* name) { get_option(name); }

  BRONTO_AFTER()
  void repl(char const* name) { get_flag(bronto::eval(KebabCase(name))); }
};

With this rewrite, the call get_option("max_retry_count") becomes get_flag("max-retry-count"). The string literal captured at each call site is substituted for name before the expression is constant-evaluated, so every call site gets its own precomputed literal.

User-defined literals

Passing a second argument makes bronto::eval emit its result as a user-defined literal. bronto::eval(value, tag) constant-evaluates value and writes it as a literal token carrying the suffix and prefix of tag. For example, given a literal operator Bytes operator""_kb(unsigned long long), a replacement written bronto::eval(n, 0_kb) writes 512_kb when n evaluates to 512.

The tag is a user-defined literal token whose value is ignored. It names three things: the literal operator to emit, the token category (e.g., character, integer, floating-point, or string literals), and the prefix. The value to emit is always the first argument. The operator is never called by the tool, so it need not be constexpr.

User-Defined Literal Example
Bytes operator""_kb(unsigned long long n);

struct ScaleReserve : bronto::rewrite_expr {
  BRONTO_BEFORE()
  void find(unsigned long long bytes) { reserve(bytes); }

  BRONTO_AFTER()
  void repl(unsigned long long bytes) {
    reserve(bronto::eval(bytes / 1024, 0_kb));
  }
};

With this rewrite, the call reserve(2048) becomes reserve(2_kb).

Supported operator forms

The tag must resolve to one of these literal operator forms, where CharT is a standard character type.

Operator formEmitted tokenExample
operator""_x(unsigned long long)integer512_x
operator""_x(long double)floating-point1.5_x
operator""_x(CharT)characteru'a'_x
operator""_x(CharT const*, size_t)stringu"abc"_x
template <FixedString s> operator""_x()stringu"abc"_x

For the character and string forms, the prefix of the tag determines which types are accepted for the constant-evaluated expression. For example, if the tag is L""_x, we will only accept a wchar_t* or a type with a constexpr data() returning wchar_t*.

The raw literal operator operator""_x(char const*) and the numeric literal operator template are not accepted. Those forms observe the token's exact spelling, and bronto::eval produces a value rather than a spelling, so it cannot guarantee what such an operator would see. A literal operator that returns an rvalue reference is also rejected, because bronto::eval's signature cannot model it faithfully.

Value types

The type of value must be one the tag's form can render as a matching token.

Operator formAccepted value types
integeran integer type other than bool, a character type, or an enumeration
floating-pointa floating-point type, or one of the integer types above
characterexactly the operator's character type
stringa string-like type, or a pointer to the tag's character type

As in the one-argument form, signed char and unsigned char are considered integer types rather than character types. For the floating-point form, an integral value must be exactly representable in the target's long double. For the string form, a string-like type is one with a constexpr data() returning a pointer to the operator's character type and a constexpr size(), and a character-pointer value is read as a NUL-terminated string, exactly as in the one-argument string form. Because a string is the only literal a class type can produce, a class-typed value is accepted only by the string form.

On this page