C++ Logo

std-proposals

Advanced search

[std-proposals] Canonical State Enforcement

From: mika.koivuranta <mika.koivuranta_at_[hidden]>
Date: Wed, 11 Feb 2026 14:49:07 +0000
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.

Received on 2026-02-11 14:49:19