| Document # | P3716R1 |
| Date | 2026-07-14 |
| Targeted subgroups | SG23 |
| Ship vehicle | C++29 |
| Reply-to | Peter Bindels <cpp@dascandy.nl> |
As part of Profiles we should have the ability to express a particular feature to be removed from the language or library in a given environment, under a particular profile.
P2759, DG OPINION ON SAFETY FOR ISO C++
R1:
class [[profiles::remove(unicode, "type is unfixably broken, use re2 instead")]] regex { ... };The type still does exist, even when the profile is active. Using the type in code to declare a variable, to use as a base class, or as part of an elaborated type specifier are flagged as ill-formed.
[[profiles::remove(unsafe, "use std::string")]]
char *strstr(const char *haystack, const char *needle);The function still exists and participates in overload selection. If the function is selected as target, even in an unevaluated context, the code is ill-formed.
[[profiles::remove_rule(unsafe, pointer_arithmetic, "No pointer arithmetic is allowed")]];When a rule is removed, any use of it is considered ill-formed as soon as it is parsed in an area with the relevant profile active.
If multiple removals are applied to a single defined entity then all of them need to be disabled for the entity to be usable. It is a better idea to define one removal, and to have multiple profiles indirect to the name under which it is removed. How such profiles interact and join is defined in the main profiles papers.
Other profiles can and should define more rules to be removed as they come up. We propose to allow referring to the name of a rule that is profile-removed as the name of a profile that removes exactly that one rule, for purposes of composition.
Pointer arithmetic can easily result in pointers outside the bounds of the original value, and is often unintended. For the vast majority of code it would help to have a blanket ban on pointer arithmetic, but some bits of code (specifically the wrapper classes similar to vector and span) must retain access to it to remain implementable.
int* a = begin(),* b = end();
size_t count = b - a; // remains legal
int* c = a + 42; // illegalC-style variadic functions (as opposed to C++-style variadic templates) do not carry any type information whatsoever and rely on incantations of va_arg, va_start, va_end etc. The correct usage of va_arg is typically encoded in a format string, which needs to be available at compile time to do this check. If the string is available at compile time, it's better to use a variadic template with compile-time format string parsing (as in <format>) - while if the format string is not available at compile time, it is a very unsafe practice that relies on having the runtime format string match with the compile-time arguments perfectly - and even in these cases, current <format> is a better choice.
Declaring functions with variadic arguments remains valid; both to keep printf's declaration valid, and to keep allowing usage of the variadic functions' very low priority in function overload selection.
printf("Hello %s\n", argv[1]); // illegal
println("Hello {}\n", argv[1]); // legalOperator comma is an operator that is hard to spot as it uses the same comma that separates normal function arguments or template arguments. As such, overloading it in general is frowned upon and discouraged for all but a very select few cases.
struct S {
template <typename RHS>
auto operator,(RHS&& rhs) { ... } // illegal
};This makes it confusing for people expecting to get the address of the object and dereferencing it again to get back to the object. The obvious-looking code *&x may do something completely different or fail to compile, and STL implementers already know to write this as *addressof(x) - but for regular users it's still better advice to never overload unary operator&.
struct S {
bool operator&() { return false; } // illegal
};Binary boolean operators have short-circuit semantics, in that they do not execute their second argument if the first argument already defines the value for the expression. This is used very often in dependent chains, such as if (p && p->value). Overloading either the binary || or binary && operator causes it to change to a regular binary operator, and will execute the second expression prior to evaluating the binary boolean operator (as it needs to pass in the result of both halves). This is surprising behavior to many, that is easily avoided by disallowing the overloads.
struct S {
bool operator&&(bool rhs) { return rhs; } // illegal
};Reinterpret_cast is a subversion of the type system that is almost unusable in regular client code. In some implementation code it is required to handle storing (for example) pointers as integers, to use low bits of aligned pointers as tag bits etc. As such, all general code usage of it should not be present.
int main() {
float f = 3.14;
return *reinterpret_cast<int*>(&f); // illegal. Also UB, but now does not compile.
}Const_cast is a tool that is intended for const correctness in a code base to be wide spread, while retaining interoperability with non-const correct code bases (such as those exposing a C-style API without thought for const). As such it should only be used on the borders of such interops, and in general code it should never be used.
Dynamic_cast is a checked cast within a virtual inheritance tree. Written as U* a; T* b = dynamic_cast<T*>(a); it can be used for downcasts (where T is a subclass of U), upcasts (where T is a superclass of U) and cross-casts (where T and U are classes that do not have one another in their superclasses). Upcasts are always safe, and compilers typically replace the dynamic_cast invocation with the static result of it. Downcasts check if the actual type is derived from T and if so return the T* equivalent, otherwise returning a nullptr; Crosscasts are cases where the two types have no obvious relation, but allow for conversion from U to T if the actual type underlying the passed-in a does implement T somewhere uniquely in its inheritance tree.
In general, code bases tend not to use many dynamic casts, and if they do a large portion of them are upcasts (sampled in 2014, code base of 7M lines, contained 128 dynamic_cast's of which 123 were upcasts or guaranteed downcasts, 4 checked downcasts and 1 crosscast). It is considered by some a "design smell" in that a "good" design uses the virtual functions to abstract away differences, seeing a dynamic_cast as a piercing of that abstraction to look at the underlying type anyway.
In those code bases, it would be desirable to restrict usage of dynamic_cast in general code.
In "Modern C++" it is possible to write code that only uses types that manage heap allocations for you, keeping your own code base free of any new or delete calls. In code bases like this it is desirable to be able to disable raw new and delete calls, so that no user code ends up doing manual memory management, even inadvertently or as a "temporary quick hack".
This does not disable creating operator new or operator delete overloads in classes, nor does it disable the ability to mark a function as =delete.
This prevents use of the output of operators that take two comparable types and that normally result in a boolean result, from being used as the input to a binary operator itself. Code like this is often written by accident as the mathematical notation looks like correct code and often even compiles cleanly. This might be a candidate for actual deprecation or removal altogether, and if so it should not remain a suppressible rule.
Often people forget that there are binary and boolean variants of the and and or operations. The binary ones are sometimes misused for the boolean ones, both as a "clever" way to avoid short-circuiting, and as a way to circumvent the nonexistence of &&= and ||= operators. These functions have well-defined behavior for this usage, but are usually not desired by being hard to read.
The C++ language allows throwing any expression as exception. Catching any exception however is done with the ... syntax, that does not allow any introspection into the error. In most large code bases, throwing anything that is not derived from std::exception is considered bad, and it would be very nice if we could enforce this code-wide.
Goto is a feature that stems from C time and that does not play well with most C++ features like RAII. If an object lifetime starts or ends between the goto point and the label it jumps to, the code is considered UB, which makes the statement itself hard to use meaningfully. In addition, it breaks structure of normal programming constructs somewhat similar to break and continue, except in an unstructured fashion making code harder to read.
In most code, switch cases are direct members of the switch statement itself. The language however does not require this, and some constructs have been created (and frowned upon actual use) that use this, specifically Duff's Device.
All of these cases can be avoided, and in nearly all code we want to prevent this from being written.
Not many people knowingly ever write empty statements, but they are somewhat regularly created by accident due to confusion about closing braces being a type declaration or a scope termination. As such, code is sometimes written that contains empty statements that serve no purpose but to confuse future authors. This rule disallows empty statements except in places where a compound-statement would also be accepted
struct S {
int a;
int b;
}; // required for closing type definition
S object;
if (object.a > object.b) {
return object.a;
}; // useless empty statement, now illegal
while (object.b--) ; // still legal empty statementMany code bases try to write portable code, and wchar_t does not have very portable behavior or encoding between most current systems. As such, the type is effectively unused on most platforms while being (and remaining) a mainstay on Windows.
Some code bases adopt the style where wrapper types or other strong type wrappers are to be used instead of small integer types. As such there should never be a hidden integer promotion in that code base - and if there is one, it's a known bug. This approach needs this ability to flag any code that accidentally still does an integer promotion.
Each removal is attached to a single profile name. When multiple removals are attached to a single profile name, they can only be switched on and off together. How profile names combine is a separate problem that is out of scope for this proposal. P3589 is the leading proposal for tackling this problem.
To be written