C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Canonical State Enforcement

From: Sebastian Wittmeier <wittmeier_at_[hidden]>
Date: Thu, 12 Feb 2026 09:15:44 +0100
Recently there was a post on this list in - as far as I understood - similar direction. FPGA simulation with C++ in discrete time steps IIRC.   -----Ursprüngliche Nachricht----- Von:mika.koivuranta via Std-Proposals <std-proposals_at_[hidden]> Gesendet:Mi 11.02.2026 15:49 Betreff:[std-proposals] Canonical State Enforcement An:std-proposals_at_[hidden]; CC:mika.koivuranta <mika.koivuranta_at_[hidden]>;  Following proposal is in markdown format: # Canonical State Enforcement  ## 1. Abstract This paper proposes a C++ standard for a compile-time language feature for expressing and enforcing *canon state* in C++.  The proposal introduces the `canon`, `noncanon` and `input` specifiers for variables and functions, enabling compile-time catching of class of bugs by splitting the program into two separate states, which C++26 currently cannot express. The feature is purely opt-in, and imposes no runtime overhead.  ## 2. Motivation A common class of errors in real-world C++ programs -- particularly in games, simulations, and interactive systems -- arises from unintended mutation of deterministic/authoritative state from outside contexts. These defects manifest as framerate dependent behavior, nondeterministic simulations, and/or desynchronization in networked or replayed systems.  There are common mitigating solutions -- namely `deltatime` and fixed-timestep execution -- but they are purely social: They rely on no developer mistakenly updating or using specific state within wrong contexts. Contexts, which they can only know by carefully examining the call tree of the entire program.  ```cpp void Move(float velocity) {     this.position += velocity * GetDeltaTime();     // oh no! GetDeltaTime() returns a value modified each frame!     // Now this summation's accuracy is dependent on framerate!     // Move(float Velocity) and this.position have both become noncanon! } ``` In this example, the `Move(float velocity)` function itself can be used as a side effect of a more complicated control flow. Thanks to the addition of wall-clock dependent time, `Move(float Velocity)` should no longer be called from, for example, deterministic physics simulations. However, the c++ standard has no enforcement for this: Only social discipline can prevent this class of bugs in C++ 26 and below.  Just like there is some state which should never be modified (`const`), there is some state which should never be modified non-canonically (`canon`). Underlying both of these specifiers is social discipline made manifest.  ## 3. Definitions **Program state** refers to all memory whose contents influence program behavior. **Canon behavior** is a function of its prior canon state and optionally of explicit user inputs. **Canon state** is program state whose mutation is intended to be deterministic: Given identical inputs, it should evolve identically across executions based on only canon state itself, and independent of factors such as hardware and rendering speed. **Non-canon state** is program state whose mutation is allowed to depend on canon, non-canon and input factors.  ## 4. Overview of the Approach The proposal introduces: * Three mutually exclusive specifiers of canonicality: `canon`, `noncanon` and `input`. These may appear in variable declarations and function declarations. * Canonicality as a property of variables and behaviors (contexts) * Rules governing mutation, assignment, operations, and call graphs * A default behavior for unannotated functions to preserve backwards compatibility  ```mermaid graph TD A[[canon main]] --> B[<noncanon> frame update] B ----> C(<noncanon> render) B ----> H[<noncanon> update inputs] B ---> E(<anonymous> std::memcpy) B --> D[<noncanon> particle effects] A --> F[[<canon> tick update]] F --> D F --> E I --> H F ----> I[[<canon> apply inputs to simulation]] F ---> G[[<canon> physics simulation]] ```  ## 5. Implementation ### 5.1. Program State  #### 5.1.1 State Rules 1. *Canon* state **may only** be modified and assigned within *canon* contexts 2. *Canon* state **may only** be initialized and assigned to *canon* and *input* states 3. *Non-canon* state **may** be initialized and assigned to *canon*, *non-canon*, and *input* states 4. Any operation involving **any** *non-canon* operands always yields a *non-canon* result (with exception of rule 12) 5. Any operation involving **only** *canon* operands always yields a *canon* result (with exception of rule 12)  #### 5.1.2 Declaration of State Canonicality Variable canonicality may be declared as: ```cpp float a = 1; // non-specified variables default to the canonicality of the context (rule 8) canon float c = 2; // canon variable noncanon float nc = 3; // noncanon variable  canon noncanon float b; // error: unknown specifier  canon float d = c; // allowed noncanon float e = c // allowed canon float f = nc // error: canon state cannot be assigned from a noncanon state (rule 2) noncanon float g = nc // allowed  noncanon float h = c + nc; // allowed; yields in a noncanon result (rule 4) noncanon bool i = c > nc; // allowed; yields in a noncanon result (rule 4) canon float j = c + d; // allowed; yields in a canon result (rule 5) ```  These rules impose a strict divide in the state of the program: Declaring `canon float c` , for example, would impose a program-wide restriction to the writing of `c`: For example, frame-dependent behavior is restricted at standard level, from accidentally modifying, the state of `c`.  However, that is only possible if all frame-dependent contexts are defined as non-canon:  ### 5.2. Contexts #### 5.2.1 Context Rules 6. *Non-canon*, and *canonically anonymous* contexts **may only** call *non-canon* and *canonically anonymous* functions 7. *Canon* contexts **may** call *canon*, *non-canon*, and *canonically anonymous* functions 8. Non-specified variables declared within a context inherit the context's canonicality. If that canonicality is *canonically anonymous*, they are *non-canon* 9. `switch`,  `if` , `for` and `while` statement bodies' context is *non-canon* if their condition is *non-canon* or *input*, or if the condition is called from a *non-canon* context  #### 5.2.2 Declaration of Context Canonicality Function canonicality may be declared as: ```cpp // note: non-specified argument types default to the global context, which is noncanon: // void y(float nc) canon // is equilevent to declaration of l()  // canon functions: void j() canon // allowed void k(canon float c) canon; // allowed void l(noncanon float nc) canon; // allowed canon float n(canon float c) canon; // allowed noncanon float m(canon float c) canon; // allowed canon float o(noncanon float nc) canon; // allowed noncanon float p(noncanon float nc) canon; // allowed  // noncanon functions: void q() noncanon // allowed void r(canon float c) noncanon; // error: modification of canon state is allowed only from strictly canon contexts (rule 1) void s(noncanon float nc) noncanon; // allowed canon float t(canon float c) noncanon; // error: assignment of canon state is allowed only from strictly canon contexts (rule 1) noncanon float u(canon float c) noncanon; // error: argument must be const (rule 1) canon float v(noncanon float nc) noncanon; // error: assignment of canon state is allowed only from strictly canon contexts (rule 1) noncanon float w(noncanon float nc) noncanon; // allowed  noncanon float example = 1;  void x() canon { j(); // allowed (rule 7) q(); // allowed (rule 7) if (example >= 5) // condition is non-canon; statement context is non-canon (rule 9) { example -= 4; j(); // error: cannot call canon functions in noncanon contexts (rule 6) q(); // allowed (rule 6) } } ```  These rules establish a unidirectional flow of authority: Canon behavior may observe and mutate any state. Non-canon behavior may observe canon state and use it to calculate its own behavior, but is not allowed to mutate the canon state.  It is good to bear in mind that calling a `canon` function is equivalent to advancing authoritative state. (e.g. Server, physics simulation, or the text on a document)  Allowing non-canon contexts to call `canon` functions allows the context to possibly by modify canon state, resulting to it being, at least partly, framerate or hardware dependent. This is exactly the developer mistake which canonicality was proposed to prevent, which is why such calls aren't allowed in this proposed feature.  #### 5.2.3. Canonically Anonymous Functions  Functions declared without either `canon` or `noncanon` are considered *canonically anonymous*. Canonically anonymous is a behavior classification which allows helper functions with side effects tied to its arguments (such as `std::memcpy`) to be called from any context.  Canonically anonymous functions act as helpers in any context, analogous to how `constexpr` functions may execute in both constant and runtime contexts.  10. *Canonically anonymous* functions' canonically non-specified arguments **may** be called with either canon or non-canon variables 11. *Canonically anonymous* functions **may** be called with non-const canon arguments, **only** if the context calling it is also canon  In short, a compiler only needs to consider the canonicality of canonically anonymous functions when such function is called with canon or non-canon arguments. In that case, the compiler only needs to check whether the function's arguments are `const` or copied, and from those which are not, whether the given values are `canon` (or assignable to canon), and if they are, whether the call context is also canon, and if it is not, then a compilation error has occurred. This allows canonicality to be fully opt-in feature, and allows programs designed with canonicality in mind to interface with libraries which otherwise lack canonical considerations.  #### 5.2.4. Declaration of Canonical Anonymity ```cpp // canonically anonymous functions: void c(float const &c); // allowed void b(float &c); // canonicality of this function and its argument depends on // whether this function is called from canon or noncanon contexts; // when called from canon context, allowed // when called from noncanon context, allowed with noncanon or const canon arguments, otherwise // error: argument must be const, or function must be canon (rule 11)  void d(canon float &c); // error: argument must be const, or function must be canon (rule 11) void e(noncanon float &nc); // allowed canon float f(canon float &c); // error: assignment of canon state is allowed only from strictly canon contexts (rule 2) noncanon float g(canon float &c); // error: argument must be const, or function must be canon (rule 11) canon float h(noncanon float &nc); // error: assignment of canon value is allowed only from strictly canon contexts (rule 2) noncanon float i(noncanon float &nc); // allowed ``` ### 5.3. Input  Canon state is defined as that which isn't affected by sources outside canon state itself. According to this definition, user input and hardware checks would be understood as non-canon. If the standard is made to enforce that type of strict canonicality, the return types of  `std::cin` and `std::fstream` would gain the `non-canon` specifier. However, this results in canon contexts with no user interaction aside from opening and closing them.  In order to preserve meaningful computing in canon contexts, the user themselves must be understood as a non-canon, yet strictly self-deterministic part of the program. For the rest of the paper, *input state* will be defined as the part of the program which that conceptual user precedes over.  A basic example of input is: ```cpp input bool buttonPressed; PushBox(canon float force) canon;  void Tick() canon {   // (imagine a non-blocking input check here) if (buttonPressed) { PushBox(200); } } ```  In the above example, the user input is a simple Boolean truth; Whether a button is pressed or not. The canonicality of such input can be easily replaced with a canon.  However, in the following example, such replacement cannot be easily made:  ```cpp noncanon float buttonPressTime; PushBox(canon float force) canon;  void Frame(deltaTime) noncanon { // (imagine a non-blocking input check here) if(buttonPressed) { buttonPressTime += deltaTime; } }  void Tick() canon { PushBox(buttonPressTime); // error: function only takes canon arguments } ``` In the above example, the user input is the **amount of time a key was pressed**. Such input's accuracy is based on input sampling rate, and so must be non-canon. This disqualifies it as a legal argument for `PushBox(canon float force) canon`  function.  This is the use case of the `input` specifier: It designates program state whose values originate from sources external to the program’s canon state, but which are nevertheless considered deterministic or authoritative.  As such, input state represents a conceptual *user*, such as keyboard input, network messages, sensor readings, or file contents. While, for example, framerate is also an external effect, it is not be considered part of the user's intent, and therefore is not of input state.  #### 5.3.1 Input Rules  12. Any operation involving **any** input operands always yields an input result (this precedes over rule 4) 13. *Input* state **may** be initialized and assigned to *canon*, *non-canon*, and *input* states  These rules implicitly specify following behavior: Input state, for all intents and purposes, acts the same as non-canon state, with one key difference: Within canon behavior, canon state is not allowed to be assigned to non-canon state, but canon state is allowed to be assigned to input state, as per rules 1 and 2.  In this sense, input can be understood as a way to "cast" non-canon state to canon state. This, sadly allows for a class of developer mistakes, wherein the developer adds `input` specifier overzealously to state which is not part of the user, or modifies input state in a way which is not part of the user's input. However, this class of mistakes are clearly marked with the input specifier and sourced by conscious disobedience to the standard, which -- in the author's opinion -- are an acceptable replacement to often hidden class of mistakes sourced from accidental disobedience to a specific codebase's social rules, caused canonical unsafety of the C++ 26 standard.  Note that there are no mentions of input contexts in this proposal: A theoretical input context would be logically equilevent to non-canon context.  #### 5.3.2 Chart of Operations Between Canonical Types  | Operand type: | Canon | Non-canon | Input |:--------|--------:|------------:|--------------:|--------------:|--------------:| | **Canon** | Canon | Non-canon| Input | | **Non-canon** | Non-canon | Non-canon | Input | | **Input** | Input | Input | Input |   #### 5.2.3 Declaration of Input Canonicality  ```cpp input bool buttonPressed; // allowed input float buttonPressTime; // allowed void InputFunction() input; // allowed  canon float c;  void Frame(float deltaTime) noncanon {     buttonPressTime += deltaTime; // error: input state may not be modified in non-canon contexts  noncanon float nc; std::cin >> nc; // allowed; noncanon state may be assigned to input state std::cin >> buttonPressed; // error: input state may not be modified in non-canon contexts  if (buttonPressed) // condition is input; statement context is non-canon { buttonPressTime += deltaTime; // allowed nc = buttonPressTime; // allowed c = buttonPressTime; // error: cannot modify canon state in non-canon context } } ```  ### 5.4. Classes Canonicality is not a property of class types. Instead, data members and member functions must be declared `canon` or `noncanon` individually. To declare a fully canon class, one must simply declare all of its members as canon.  ### 5.5. Templates  Template declarations inherit the canonicality of their primary declaration. All instantiations of a given template share the same canonicality.  ## 6. C++ Code Example ```cpp canon float time; float frameTimer = 0.1; // global scope defaults to noncanon  canon const float tickTimer = 0.2;  void sideEffect(float& arg) // canonically anonymous function: Can be called in canon and noncanon as long as arguments arent canonically illegal {     arg = 5; }  template <typename T> T CalculateNewTime(T time) // canonically anonymous function: Can be called in canon and noncanon as long as arguments arent canonically illegal {     return 0.1; }  struct Foo // cannot declare canon or noncanon on structs/classes {     float a = 0; // global scope defaults to noncanon     noncanon float b = 1;     canon float c = 2;      Foo() {}     Foo(float nc) : c(nc) {} // error: cannot use type "noncanon float" to set the value of type "canon float"     Foo(canon float nc) : c(nc) {} // allowed };  void Frame() noncanon  // noncanon function: Can call noncanon and anon functions {     noncanon float newTime = time; // allowed     canon float* p; // allowed     p = &newTime; // error: cannot modify canon state within noncanon behaviour     noncanon float* b; // allowed     b = &time; // error: cannot use type "canon float*" to set the value of type "noncanon float*"     time = 0; // error: cannot modify canon state within noncanon behaviour      Foo* foo = new Foo(); // TODO: Should default initialization of canon members be allowed in noncanon contexts?     Foo* foo = new Foo(2); // error: cannot modify canon state "canon float c" in noncanon behaviour     // note: literal "2" defaults to context's canonicality, which in this case is noncanon      foo->a = foo->c + 3; // allowed     foo->c = 3; // error: cannot modify canon state within noncanon behaviour      sideEffect(time); // error: canonically anonymous function cannot take non-const canon arguments while called from a non-canon context     sideEffect(newTime); // allowed      if (newTime == time) // any operation involving any noncanon operands always yields a noncanon result; The condition is noncanon -> the statement context is noncanon     {         p = &newTime; // error: cannot modify canon state within noncanon behaviour     }     if (time == 0) // any operation with only canon operands always yields a canon result; The condition is canon, but is in non-canon context -> the statement context is non-canon     {         p = &newTime; // error: cannot modify canon state within noncanon behaviour     } }  void Tick() canon  // canon function: Can call canon, noncanon and anon functions {     noncanon float newTime = time; // allowed     canon float* p; // allowed     p = &newTime; // allowed     noncanon float* b; // allowed     b = &time; // error: cannot use type "canon float*" to set the value of type "noncanon float*"     time = 0; // allowed      Foo* foo = new Foo(); // allowed     Foo* foo = new Foo(2); // allowed     // note: literal "2" defaults to context's canonicality, which in this case is canon      foo->a = foo->c + 3; // allowed     foo->c = 3; // allowed      sideEffect(time); // allowed     sideEffect(newTime); // allowed      if (newTime == time) // any operation involving any noncano operands always yields a noncanonresult; The condition is noncanon -> the statement context is noncanon     {         p = &newTime; // error: cannot modify canon state within noncanon behaviour     }     if (time == 0) // any operation with only canon operands always yields a canon result; The condition is canon -> the statment context is canon     {         p = &newTime; // allowed     } }  int main() canon // canon function: Can call canon, noncanon and anon functions {     float tickTime = 0; // defaults to canon, since defined within canon context      while (true)     {         time = CalculateNewTime<float>(time); // canonically anonymous called with non-const canon state in canon behaviour -> allowed!          if (time >= frameTimer) // any operation with non-canon operands always yields a non-canon result; condition is non-canon -> statement context is non-canon         {             Frame(); // noncanon function call         }          tickTime += time; // modification of canon allowed in canon function         while (tickTime >= tickTimer) // any operation between only canon operands always yields a canon result; condition is canon, and is in canon context -> statement context is canon         {             tickTime -= tickTimer;             Tick(); // canon function call         }     } } ```  ## 7. Backwards Compatibility Due to the proposed anonymous canonicality , the behavior of programs which do not use canonicality (`canon`, `noncanon` and `input` specifiers) would be unaffected. Canonicality is entirely opt-in and introduces no changes to the building and running of existing programs and libraries, except in case of macros using the names of the specifiers proposed in this paper.  ## 8. Issues The most common argument against any standard addition is that a programmer could reasonably design it themselves with libraries, or with wrappers to another language. But in the case of canonization, this argument becomes an expectation for each compiler to have their own specification for canonicality, or for them to leave standard C++ to be fundamentally canonically unsafe. Both choices result in complete developer uncertainty, which seems an unnecessary and harsh punishment for simply using C++ for any low-level code.  ### 8.1 Why not use types? Types can express canonized variables; `canon_float`, `noncanon_float` and `input_float`can express  the full canonicality of a `float` type, even though it essentially quadruples all primitive types.  However, types cannot express canonized functions: `canon_float function()` is not equivalent to `float function() canon`. Types also cannot specify canonically anonymous functions.  ### 8.2 Why introduce canonicality to the whole standard?  Existing approaches rely on social discipline: Conventions which prevent the bug, or laborious manual checks or code reviews to catch the bug. This proposal provides compile-time enforcement comparable in spirit to `const` or `static_assert`, which also exist to apply important code conventions to the language itself.  While canonization mainly helps in physics simulations, any program with authoritative, deterministic state would only gain from canonization: A library which allows online editing of a text-file, or handling of user input, for example, can only safeguard their state by hiding it from the user completely (in a nameless namespace, or by making the state private). However, canonization allows for internal state to be marked, and its modification enforced to the exact contexts where such modification is allowed.  Canonization also offers a solution to functions and variables which are intended to be deterministic: Pseudo-random number generators, timers and physics objects often allow modification at runtime, which runs the risk of a developer mistakenly modifying them non-deterministically. With canonization, programmers may explicitly declare functions as deterministic, by using the `canon` specifier.  In short, canonicality should be used in any case where the **mutability must be limited to specifically deterministic behavior**.  ### 8.3 How does canonicality affect the standard library?  #### 8.3.1 Canon  Canonicality is defined through the developer's intent, and so shouldn't be added to the standard library. Canon specifier is also opt-in, and so shouldn't be forced upon the developer. Furthermore, canon specifier breaks backwards combability in libraries it is used in.  It is important to let the user be free to define the state which they intend enforce as canon.  #### 8.3.2 Canonically Anonymous  Standard helpers, such as:  ```cp std::sort std::copy std::transform std::accumulate std::memcpy std::memmove ```  and pure functions such as:  ```cpp std::sin std::cos std::sqrt std::min std::max std::mt19937 ``` Should stay canonically anonymous, since their return types and side effects depend on call context, and such are allowed in every context.   #### 8.3.4 Non-canon  Non-input reading functions based on time or environment, such as:  ```cpp std::chrono::system_clock::now() std::clock() std::random_device() std::getenv() ``` Are definitionally non-canon, since they depend on state outside of the program. The functions and their return types should both be non-canon.  Note that the user is free to assign any of these into canon or input state, choose they disagree with what makes up the conceptual user within their programs.  #### 8.3.3 Input  All of the standard library's input-reading functions, such as:  ```cpp std::cin std::ifstream std::filesystem::last_write_time ```  Should specify their return types as `input`. The functions themselves should be non-canon. Streams are not necessarily input and should stay with their default context-dependent canonicality.   ### 8.4 Why not enforce deterministic while loops?  This is possible: `while` and `for` loops can be checked at compile-time for determinism:  ```cpp const int forever = true; // const while (forever) // non-deterministic, runs based on execution speed, statement context should be non-canon { //... } ```  However, this is a form of the unsolvable halting problem, and therefore one's work on this feature will never be finished. However, some trivial non-halting contexts, such as bare while loops, could be enforced within the standard as strictly non-canon contexts. though such algorithms are outside the scope of this paper.  Another solution is to handle `for` and `while` loops as non-canon contexts by default. This, however, limits the possible behaviors of canon behavior. Instead, allowing `canon` and `noncanon` specifiers to apply to `for` and `while` statements themselves gives the control fully to the developer, though again, such feature is outside the scope of this paper.  ### 8.5 Open issues:  * Should the specifiers `canon`, `noncanon` and `input` be attributes? * Names of the specifiers `canon`, `noncanon` and `input` should probably be changed for sake of brevity * Exact error wording is unfinished * Should we allow initialization of canon members within non-canon behavior? *  `goto` might allow contexts to break out canon behavior, but is that a real problem?  ## 9. Conclusion Canon state exists implicitly in all programs but is not representable or enforceable in current C++. This proposal introduces changes to the standard library, as well as a minimal compile-time, opt-in feature which expresses and enforces authority boundaries at compile time, preventing a large class of common bugs without a significant cost.   -- Std-Proposals mailing list Std-Proposals_at_[hidden] https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2026-02-12 08:32:44