Field pattern matching

To find and replace non-static data members, create a struct that inherits publicly from bronto::rewrite_field and use the BRONTO_BEFORE() and BRONTO_AFTER() annotations to identify fields and define their replacements.

Example
struct IntToLong : bronto::rewrite_field {
  // Matches any data member of type `int`, in any class.
  template <typename Class>
  BRONTO_BEFORE()
  void before() { int x; }

  // Rewrites it to have type `long`.
  template <typename Class>
  BRONTO_AFTER()
  void after() { long x; }
};

Field patterns work much like declaration patterns. The body of each function declares a single variable standing in for the field, and the name of that variable is irrelevant.

The class template parameter

Every function marked BRONTO_BEFORE() or BRONTO_AFTER() must declare a type template parameter first. Bronto binds it to the class, struct, or union that declares the matched field. It must not be a pack and must not have a default argument.

A pattern can name the class to match only fields whose type refers to their own class, and a replacement can name it to spell the class at each match.

Example
struct SelfPointerToConst : bronto::rewrite_field {
  template <typename Class>
  BRONTO_BEFORE()
  void before() { Class* x; }

  template <typename Class>
  BRONTO_AFTER()
  void after() { Class const* x; }
};

struct List {
  List* next;   // Rewritten to `List const* next;`
  Node* other;  // Not matched
};

A constraint on the class template parameter, or a call to bronto::where, restricts which classes have their fields rewritten.

Example
struct OnlyInAggregates : bronto::rewrite_field {
  template <typename Class>
  BRONTO_BEFORE()
  void before() {
    bronto::where(std::is_aggregate_v<Class>);
    int x;
  }

  template <typename Class>
  BRONTO_AFTER()
  void after() { long x; }
};

Initializers

A field can be initialized in two ways: by its default member initializer or by the member initializer list of each constructor. A pattern with an initializer matches initializers written in either place.

Example
struct ZeroToNullptr : bronto::rewrite_field {
  template <typename Class>
  BRONTO_BEFORE()
  void before() { int* x = 0; }

  template <typename Class>
  BRONTO_AFTER()
  void after() { int* x = nullptr; }
};

struct S {
  S() : p(0) {}    // Rewritten to `S() : p(nullptr) {}`
  S(int) : p{0} {} // Rewritten to `S(int) : p{nullptr} {}`
  int* p = 0;      // Rewritten to `int* p = nullptr;`
};

Initializers match as they do in bronto::rewrite_decl. That is, they can be annotated as BRONTO_BEFORE("loose") or BRONTO_BEFORE("strict"). Under loose matching, the initialization style is ignored, so a pattern matches initializers regardless of whether it is initialized with braces, parentheses, or =. Under strict matching a pattern matches only initializers written with a matching style.

Strict patternDefault member initializerMember initializer
T x = e;T x = e;Never
T x = {e};T x = {e};Never
T x{e};T x{e};x{e}
T x(e);Neverx(e)

Each initializer is matched on its own, binding the parameters of the pattern anew. Every initializer the pattern matches is rewritten, and the others are left alone. The declaration of a field is rewritten when at least one of its initializers matches. This is to facilitate rewrites that might span multiple translation units (for example, if a class has its constructors implemented outside of a header).

Because a rule will generate rewrites even when not all initializers match (or are even visible in the translation unit), care should be taken to ensure that the desired behavior occurs. This means either ensuring that changes from all relevant translation units are applied simultaneously, or that changes can be safely applied while only updating some of initialization sites.

Deleting fields

A BRONTO_AFTER() function with an empty body deletes each matched field. Every member initializer naming the field is deleted along with it, including those the pattern would not have otherwise matched.

Example
struct DeleteUnusedCounter : bronto::rewrite_field {
  template <typename Class>
  BRONTO_BEFORE()
  void before() { UnusedCounter x; }

  template <typename Class>
  BRONTO_AFTER()
  void after() {}
};

struct S {
  S() : counter(1), size(0) {}  // Rewritten to `S() : size(0) {}`
  S(int n) : counter(n) {}      // Rewritten to `S(int n) {}`

  UnusedCounter counter;        // Deleted
  int size;
};

Bronto does not check whether the deleted field is still used, so such a rule should only match fields with no remaining uses. This can be done with a BRONTO_USAGE(forbidden) rule, forbidding the deletion if there are any remaining uses of the field.

Bronto also does not check whether the initializer of the deleted field has side effects. The assumption is that if you are deleting a field, the computation of that field, along with any side effects it may have, are no longer needed and subject to deletion as well.

BRONTO_USAGE

Field patterns may have nested structs annotated with BRONTO_USAGE(...), which constrain how the field is used. They work as they do for declaration patterns. A nested bronto::rewrite_expr rule matches expressions naming the field, such as x in a member function or s.x and p->x elsewhere. A nested bronto::rewrite_decl rule matches variables initialized from the field. Member initializers are not uses.

Example
struct DeleteUnusedCounter : bronto::rewrite_field {
  template <typename Class>
  BRONTO_BEFORE()
  void before() { UnusedCounter x; }

  template <typename Class>
  BRONTO_AFTER()
  void after() {}

  struct BRONTO_USAGE(forbidden) AnyUse : bronto::rewrite_expr {
    BRONTO_BEFORE()
    UnusedCounter find(UnusedCounter x) { return x; }
  };
};

struct S {
  S() : counter(1), size(0) {}  // Rewritten to `S() : size(0) {}`

  UnusedCounter counter;        // Deleted, as nothing reads it
  UnusedCounter other;          // Kept, as `Other` reads it
  int size;

  UnusedCounter Other() const { return other; }
};

Uses are found in each translation unit separately, and the edits from every translation unit are combined. A BRONTO_USAGE(forbidden) constraint therefore only prevents a rewrite in the translation units that see the forbidden use. A field declared in a header and read in one source file may still be rewritten from another source file including that header.

Limitations

  • The body of a BRONTO_AFTER() function must either be empty or declare a single variable and nothing else, as there is nowhere around a data member to place statements.
  • The variable declared by a BRONTO_AFTER() function must spell out its type. A data member cannot be declared with auto, decltype(auto), class template argument deduction, or an array without a bound.
  • Bit-fields are not matched.
  • Field rewrites cannot be nested within another rule as a usage constraint, and bronto::rewrite_lambda_capture rules cannot be usage constraints on a field rewrite.
  • Only constructors defined where the class is visible are considered. A field whose constructors are defined in another file is matched against the initializers each translation unit can see, and the edits found in every translation unit are combined.
  • Member initializers in constructor templates and designated initializers in aggregate initialization are not matched.

On this page