C++ Logo

std-proposals

Advanced search

Re: [std-proposals] caller_return - a step in the direction of nicer error-by-value

From: Lorand Szollosi <szollosi.lorand_at_[hidden]>
Date: Thu, 4 Dec 2025 23:25:00 +0100
Hi,

A little late for the party it might seem, but hear me out - your idea
makes perfect sense and, in fact - with a little bit of polishing and
call-side markup - it could be rolled out with what I wrote in Febr, 2017
(see old archives). Furthermore, it's a typical issue that comes up in many
forms and many proposals, including, but not limited to, Statement
Expressions, for-else, inline exceptions, inline try, simplified lambdas,
etc. Strangely, these face a resistance on this list and I don't know why -
it was Bjarne Stroustrup's word that C++ is a multi-paradigm language and
tries to stay as such (2015, C++ Meetup call, Budapest) and these are
nothing else than monadic programming, continuation monad manipulation.

So, without further delay, this is what you could do in case the very basic
case of Statement Expressions were voted in - that's already a part of gcc,
clang, icc, Sun-now-Oracle and IBM - to my current information, only msvsc
is missing from the list among the main compilers.

std::variant<Ret, Err> foo() noexcept {
    auto result {someFailableOperation()};
if (!result)
        return Err{result};
    std::println("The result: {}", *result);
    return Ret{};
}

void process_further(Ret);

bool f() {
    // call site
    process_further( ({ auto foo_res = foo();
                        if (std::holds_alternative<Err>(foo_res)) // can
also be written generically as std::get_if<1>(foo_res)
                            return false; // returns from f()
                        std::get<0>(std::move(foo_res)) }) );
}

Note that, while it seems extra work at call site, it can be collapsed into
a macro, something like WITH_RETURN:

#define WITH_RETURN(...) ({ auto res = __VA_ARGS__; \
                            if (auto p_retval_caller = std::get_if<1>(res))
 \
                                return std::move(*p_retval_caller);
                            std::get<0>(std::move(res)) })

auto f2() {
    process_further(WITH_RETURN(foo()));
}

This is nothing worse than the classic return_if() found in many codebases
for error code handling in old C codes. I'd argue it's even readable.
What we really try to emulate here is 'pass' the return continuation: that
is, imagine as if 'return' was a function call that takes the current
'stack' (where stack is not a part of the standard, but we have a common
understanding of it as all previous nestings with current state); then
permit passing it around.

For completeness, it's also possible to rewire break and continue
continuations as long as somebody is willing to accept it on an actual code
review:

// NOTE - OK TO SKIP on first read


#include <iostream>

#include <variant>


struct t_break {};

struct t_continue {};


#define WITH_BREAK_AND_CONTINUE for (;; ({ return t_break{}; }))
for (;; ({ return t_continue{}; }))


#define CAN_BREAK_AND_CONTINUE(...) \

    ({ \

        auto tmp = __VA_ARGS__; \

                                                        \

        if (std::holds_alternative<t_break>(tmp)) \

            break; \

                                                        \

        if (std::holds_alternative<t_continue>(tmp)) \

            continue; \

                                                        \

        std::get<0>(std::move(tmp)); \

    }) \


std::variant<int, t_break, t_continue> /*int [[with::break,
with::continue]]*/

fn(int i)

{

    WITH_BREAK_AND_CONTINUE

    {

        if (i % 2) continue;

        if (i > 5) break;

        return i;

    }

}


int main()

{

    for (int i = 0; i < 10; ++i)

    {

        std::cout << CAN_BREAK_AND_CONTINUE(fn(i));

    }


    std::cout << std::endl;

}



This might be valued anywhere between 'nice hack' and 'never ever' in your
scale; the goal here is to present what kind of monster we're dealing with:
basically, continuation management. These are sub-cases of the classic
call/cc paradigm. The above code might present itself in alternative
implementations of for-loop over ranges (those that have special cases for
first, last, odd, even, etc. elements, or an else case for empty ranges),
typically not directly written in application code.

A similar use-case, from a different area is unevaluated expressions, or
call-by-name as ancient languages call it. A typical deficiency of C++ is
that we cannot have proper overloads for short-circuiting operators: while
e.g. a || b can short-circuit for bools (or bool-convertible expressions),
it's impossible currently to write an overload for custom classes that does
this short-circuiting with the exact same syntax (i.e., for A calc_a(); B
calc_b(); the expression calc_a() || calc_b() will always call both
functions, being both suboptimal in performance and producing possibly
unwanted side-effects of the calls). If call-by-name were introduced, we
could pass the actual processing for the alternative path and propagate it.
Currently, we emulate this by passing lambdas, which is suboptimal.
Consider which is easier to comprehend (actual code from test cases of my
implementation):
// with unevaluated:

int f(unevaluated<int> x, int y) { /* use x and y */ }


int g() {

  int a = 40;

  return f(a + 2, 1);

}


// without unevaluated:

int f(auto x, int y) { /* use x() and y, track when to evaluate x() */ }


int g() {

  int a = 40;

  return f([&]{ return (a + 2); }, 1);

}


A further use-case is generic continuation monad, which, in terms of c++
and overloading, converts f(x) to x(f) for monadic x, or, in general,
converts f(pre..., x, post...) to x([&](auto& x0) { return f(pre...,
std::forward<decltype(x0)>(x0), post...); } . This propagates the error,
similarly to try expressions proposal, but to the uppermost expression
level (and could be extended amongst expressions / statements, if we wanted
to do so). A benefit of this, beside immediately having access to monadic
paradigms, decoupling error handling from main processing even more than
exceptions do, monadic extensions of types (like, a data series in-terms-of
elements), better syntax for interpolation / monte-carlo that takes a
considerable portion of finance programming (think of expressions like g(1,
2, 3) converted to g(1, data_series, 3) with implicit looping, or even
implicit equation solving), so another benefit is simplification of the
standard. A great portion could be written in a monadic way - not sure if
this is amongst the goals, but it's surely a possibility - and further
proposals could be written *and, in many cases backported* via monadic
definitions. Actual code, simplified form, in my private codebase:

template<typename T>

struct my_cpassable_monad

{

    T i;


    my_cpassable_monad(T i) : i(i) {}


    template<typename F>

    cpass<my_cpassable_monad<std::invoke_result_t<F, T>>> operator()(F&& f)
const;


    operator T() const; // not called normally


    static cpass<my_cpassable_monad<T>> ret(T i) { return
my_cpassable_monad<T>(i); } // named ctor, cpass<T> denotes the
transformation

};



int f(int x, int y);



int main() // new, cpass<>-based code

{

    std::cout << f(1, f(2, my_cpassable_monad<int>::ret(0))) << " * " <<
std::endl;

}

Consider the ugliness we'd have to write without monads here, in main:

int main() // old code without cpass<>'s assistance

{

    my_cpassable_monad<int>::ret(0)([&](auto&& cval) -> decltype(f(2,
std::forward<decltype(cval)>(Val))) { return f(2,
std::forward<decltype(cval)>(cval)); })([&](auto&& cval) -> decltype(f(1,
std::forward<decltype(cval)>(cval))) { return f(1,
std::forward<decltype(cval)>(cval)); })([&](auto&& cval) ->
decltype(std::cout << std::forward<decltype(cval)>(cval)) { return
std::cout << std::forward<decltype(cval)>(cval); })([&](auto&& cval) ->
decltype(std::forward<decltype(cval)>(cval) << " * ") { return
std::forward<decltype(cval)>(cval) << " * "; })([&](auto&& cval) ->
decltype(std::forward<decltype(cval)>(cval) << std::endl) { return
std::forward<decltype(cval)>(cval) << std::endl; });

}

I sincerely hope we understand the cpass<>-based easier to follow and
maintain.

So,

Basic statement expressions (i.e., the first part of my old proposal) is
available in many current compilers. For unevaluated and cpass expressions
(i.e., monads), I have a preliminary implementation via a custom,
clang-based precompiler (that emits C++ code processable by other compilers
as well). I sincerely think it'd be great to pick up on these topics,
perhaps extend them to statements and control structures (which was among
the goals of coroutines as I read in 2017, until the scope was set to
current). It'd help testing, maintenance and even some specific set of live
codes. It's an alternative to error codes, exceptions, error continuations,
try-expressions and explicit std::expected<> use, while provides much more
control at zero additional cost.

Thanks for reading,
Lorand Szollosi
-lorro


On Thu, Jul 31, 2025 at 4:35 PM Ayrton Lachat via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Hello,
>
> This proposal is about adding a way for a callee to return from the
> caller. Such a feature can be useful to avoid the multiplication of
> `if`(-`else`) blocks related to error handling, and more specifically the
> one that just forwards the error. For example, the following code :
>
> ```cpp
> std::expected<std::string, int> someFailableOperation() noexcept;
>
> std::expected<void, int> foo() noexcept {
> auto result {someFailableOperation()};
> if (!result)
> return result.error();
> std::println("The result: {}", *result);
> return {};
> }
> ```
>
> would become the much nicer :
>
> ```cpp
> std::expected<void, int> foo() noexcept {
> std::println("The result: {}", unwrap(result));
> return {};
> }
> ```
>
> This `unwrap` function could even in the future be replaced by some
> operator (question mark ? certainly, but not sure if this should be in the
> same proposal).
>
> First, we need to define some goals of this feature to make it play nice
> with existing code and for it to not create disturbing workflow :
>
> 1. A function capable of *caller-return *must be tag as such in the
> prototype (even make it part of the type, like for noexcept?). In this
> way, not every random function can start doing it, and the
> abi-compatibility is preserved (as such a change would certainly require
> abi change).
> 2. Having a way to get the *caller-return-type *in the callee, so we
> can make conditional error handling (mostly conversion).
> 3. Having a way to force your function to be used only when the
> *caller-return-type* match certain requirements.
>
>
> With those requirements, I propose the following syntax :
>
> ```cpp
> (1) ‎caller_return < _type-parameter_ > _function-declaration_
> (2) caller_return < _type-parameter_ > requires _constraint_
> _function-declaration_
> ```
>
> Where:
>
> - *type-parameter *: a single type parameter, may be constrained
> - *function-declaration *: a usual function declaration, may be
> templated
> - *constraint *: some constraint on the *type-parameter*. The
> requires-clause is merged with the one from usual template, if the function
> is templated.
>
>
> A function mark _caller-return_ is implicitly _inline_.
>
> And we add the following usage for the `caller_return` keyword, inside a
> function scope :
>
> ```cpp
> (1) ‎caller_return;
> (2) caller_return _expr_;
> ```
>
> When this instruction is encountered, the caller is returned from, with
> the value of *expr* (or void for (1)). The type of the
> caller-return-value must meet the same requirements as if it was used in a
> normal return statement in the caller.
>
> Of course, this usage of the keyword is limited to function marked
> caller-return.
>
> With this syntax, the `unwrap` function of the example at the top can be
> written as follow :
>
> ```cpp
> caller_return</* is-expected-concept */ CallerReturn>
> template <typename Res, std::convertible_to<typename
> CallerReturn::error_type> Err>
> constexpr Res unwrap(std::expected<Res, Err> exp) noexcept {
> if (exp)
> return *exp;
> caller_return std::unexpected(static_cast<typename
> CallerReturn::error_type> (exp.error()));
> }
> ```
>
> There is still the question of which functions are allowed to be
> caller-return. Destructor should definitely not be caller-return. What
> about constructor? I don't have an opinion on whether they should be
> allowed or not. Operator overloading? One of the main appeals of this
> feature is the possibility to allow implementation of a question mark
> operator, so operator overloading should be allowed. But using it with the
> current operator can make the flow of a function not obvious at all
> (because who read the prototype of operators). And people will certainly
> use it on the dereference operator, which is not a behavior most C++
> developer expect. So, I think it should be disallowed on operator
> overloading, except for a question mark operator (if one is added).
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2025-12-04 22:25:19