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.
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.
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.
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.
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 pattern | Default member initializer | Member 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); | Never | x(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.
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.
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 withauto,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_capturerules 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.