---
title: "Do do_return !"
document: P4323R0
date: 2026-08-11
audience: EWG
reply-to:
  - "Jan Schultke <janschultke@gmail.com>"
---

[[P2806R4]](https://wg21%2elink/p2806r4) proposed to let the user omit the last semicolon in a `do` expression to yield a result. This paper argues against that feature for a variety of reasons.



## Problems with omitting `do_return`

### Novelty and inconsistency

The ability to omit keywords like `return` which would yield results from expressions is a staple of other languages, such as Kotlin and Rust.

> Kotlin implicitly uses the last statement of a block as the result, in various places:
> 
> ```cpp
> val x = run { // run invoked with a lambda
> println("Hello")
> 42
> }
> val y = if (true) {
> // ...
> "yes"
> } else "no"
> ```

> In Rust, the last statement of a block (not suffixed by a semicolon) is the result of a block:
> 
> ```cpp
> let x = {
> let a = 3;
> let b = 4;
> a + b // value of the block because semicolon is omitted
> };
> let y = if true {
> "yes" // value of the block because semicolon is omitted
> } else "no";
> ```

The key observation is that in those other languages, the last statement is result design is consistently applied across the language, for a wide variety of constructs. This is not the case in [[P2806R4]](https://wg21%2elink/p2806r4), making that design novel and surprising for `do` expressions. C++ users have been taught for 30 years that

- blocks enclosed by braces contain a bunch of statements,
- you put a semicolon at the end of the line (unless the line starts with `if` or whatever), and
- control flow interactions (without which you flow off the end of a block) require a keyword like `if`, `return`, `break`, `co_yield`, etc.

There is an immense amount of value in these consistent and teachable rules, which [[P2806R4]](https://wg21%2elink/p2806r4) proposes to weaken, for the purpose of reducing verbosity. These rules would no longer be universal rules.

Furthermore, why is the design not applied to other constructs, even though it would be possible in principle?

> The proposed feature could just as well be applied to lambda expressions:
> 
> ```cpp
> int x = do { 42 };
> int y = [] { 42 }(); // equivalent to above
> ```

There are two possible directions here:

- Not changing lambda expressions would be inconsistent with `do` expressions, without good rationale. We would still sacrifice the consistent and teachable rules by making `do` expressions special, but we wouldn't get much value in return. Verbosity of lambdas is a common complaint.
- Changing lambda expressions like this retroactively might be fine, but it demonstrates that the last statement is the esult feature is orthogonal to `do` expressions, and that the design needs to be explored holistically.

Either way, the conclusion seems that [[P2806R4]](https://wg21%2elink/p2806r4) should not be proposing this design. Perhaps it could be done in a separate paper for various constructs in general.

### Competing styles

An inevitable outcome of letting users omit `do_return` is that there will be competing styles, where some users prefer to always write `do_return`. This would be motivated by consistency with control flow in other places, like `return` and `co_yield`. A C++ developer used to omitting `do_return` would find it jarring to work on a code base where `do_return` is always required, and vice versa.

While having some disagreement over style is natural in a programming language, C++ already has a ton of options without a clear winner (initialization style, trailing return types, when to use `auto`, etc.), and this issue would be compounded. There is real cost to adding more and more competing styles which then either need to be litigated by each code base or lead to inconsistent code within one code base.

### Visually grepping control flow

In the following code block, try to find all the places where control flow is altered in some way:

```cpp
bool members_equal(
std::span<const Group_Member_Value> xs,
std::span<const Group_Member_Value> ys,
const File_Source_Span& lhs_location,
const File_Source_Span& rhs_location,
Context& context
)
{
if (xs.size() != ys.size()) {
return false;
}
for (std::size_t i = 0; i < xs.size(); ++i) {
const auto name_equal = evaluate_internal_equality(
xs[i].name, ys[i].name, lhs_location, rhs_location, context
);
if (!name_equal || !*name_equal) {
return false;
}
const auto value_equal = evaluate_internal_equality(
xs[i].value, ys[i].value, lhs_location, rhs_location, context
);
if (!value_equal || !*value_equal) {
return false;
}
}
return true;
}
```

Toggle blur

Despite this block being blurry and thus completely unreadable, you can actually identify all the `if`, `for`, and `return` statements easily do to their shape and syntax highlighting. The same visual identification of control flow is done subconsciously by developers all the time. Searching for a `return` statement in a long function doesn't require reading the entire function from top-to-bottom, but rather scanning for the shape and color of a `return` statement.

As things stand, *universally*, builtin control flow constructs in C++ can be visually identified in this way. This would not be the case for `do_return`, which can be omitted.

### Accessibility problems

In the proposed design, there is a major difference between the following expressions:

```cpp
do { f() } // expression of the type of f's return value
do { f(); } // expression of type void
```

This design is hostile to users with poor eyesight because a semicolon has major semantic impact. It also shows that having competing styles is not just a matter of preference; one user may prefer to omit `do_return` because they have 20/20 vision and they optimize for brevity, which is detrimental to other users with poor eyesight.

The design also poorly integrates with screen readers because it requires keeping the screen reader in high verbosity mode to read code. Historically, C++ semicolons could have been largely omitted as noise because they were merely statement terminators, with a few exceptions like `for (;;)`.

> That is, `int x = 0; int y = 0;` could have been pronounced as int x equals zero int y equals zero without loss of information.

### Textually grepping control flow, future direction

Another way to scan for control flow is to perform a text search. If the user is confronted with a long `do` expression and wants to find all the places where it yields a result, they can search for `do_return` and jump from search result to search result. This is not possible when the only distinguishing factor between yielding a result and yielding `void` is a semicolon.

Perhaps initially, the issue is not so bad because the user only has to check the last statement of a `do` expression to see if it yields a result. However, a logical next step in language design is to allow the following:

```cpp
auto x = do {
if (condition) { // result of if as a whole is an int,
1 // which is the result of the do expression
} else {
2
}
};
```

This wouldn't be a breaking change because yielding a result of the `if` expression requires omitting semicolons inside the `if` statement, which is currently not possible.

Once again, we are confronted with uncomfortable choices:

- If we want to pursue omitting `do_return` in more places, we should commit to it fully, and consider the consequences for all possible statements rather than just the last statement in a `do` expression. There is a slippery slope here.
- If we don't want to pursue this, the current design of [[P2806R4]](https://wg21%2elink/p2806r4) is half-baked. If Rust and Kotlin let you omit the `do_return` equivalent in more places, why can't we? Why would Rust be such an inspiration for the paper if we cherry-pick only a small portion of its syntax and consider the same Rust syntax in other construct to be a bad fit for C++?

### Irregular `void` and latent bugs

Another observation is that in Kotlin and Rust, the following construct works fine, when it doesn't in C++:

```cpp
fun f() { } // Kotlin function that returns Unit (like C++ void)
val x = run {
f() // fine, result is Unit
}
```

```cpp
fn f() { } // Rust function that returns ()
let x = {
f() // fine, result is ()
};
```

```cpp
void f() { }
auto x = do {
f() // error, cannot have variable of type 'void'
}
```

Consequently, the existence of the semicolon becomes hugely significant, especially in templates, where diagnostics are delayed until instantiation.

> ```cpp
> void f(std::invocable</* ... */> auto action) {
> // Nonsensical code assuming 'action' returns 'void',
> // but mistake is not diagnosed until instantiation:
> auto x = do { action() /* forgot semicolon */ };
> }
> ```
> 
> If the user forgot to append a semicolon to the last statement but they intended the `do` expression to be a `void` expression, their code would inadvertently return something unexpected, and storing the result in a variable would compile even if it makes no sense. By comparison, it is much less likely that the user forgets prepend `return` before an expression.
> 
> `42;` is obviously wrong when you're used to seeing a big, syntax-highlighted `return` at the end of your blocks.

> ```cpp
> void f(std::invocable</* ... */> auto producer) {
> auto x = do { producer(); }; // error: 'auto' deduces to 'void'
> }
> ```
> 
> If the user accidentally added a semicolon (perhaps out of habit or due to muscle memory), their code would not compile because there is no regular `void`, so the result couldn't be stored in `auto x`. Again, the fact that there is a stray semicolon is much less obvious than a missing `return` or `do_return`.
> 
> Currently, this can be diagnosed prior to instantiation because the `do` expression must return `void` based on its syntax and `void` is not a valid type for `x`, but this could change in the future if `void` was made more regular in the language.

## Conclusion

The discussion above shows that there are many serious problems with omitting `do_return` in `do` expressions. Consistency with other constructs, teachability, stylistic consistency, accessibility, visual and textual greppability, and other positives aspects of the language are affected where `do` expressions are used without `do_return`.

Either, that direction should not be pursued at all, or it should be pursued holistically. That is:

- Do we want to allow omitting the `return` statement in lambdas, and/or in functions, in general?
- Do we want to allow omitting `do_return` in the last `if` and/or `try` statement of a `do` expression? What if we want to make `if` and/or `try` an expression in the future?
- What other language constructs are affected by this, now and in the future?
- How do these decisions impact overall teachability, greppability, and accessibility of C++?

If we simply ignore these concerns, we risk losing sight of the big picture. This is unjustifiable, especially considering that the omission of `do_return` can be introduced at any point in the future without breaking changes, since the omitted-semicolon syntax used by the feature is currently ill-formed.

## References

[P2806R4]

Bruno Cardoso Lopes, Zach Laine, Michael Park, Barry Revzin.

do expressions

2026-07-15

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p2806r4.html
