The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal

Execute a function after a return statement

Notes published the
8 - 11 minutes to read, 2120 words
Categories: c c++
Keywords: c c++ return value optimization

Recently I had a function that accepted a callback as a parameter, and I needed to add an action after executing it. The "issue" is that the callback can return arbitrary data, that should be returned to the caller.

The code looked like this:

struct s{
	std::vector data;

	template <class callback>
	delctype(auto) fun(callback c) {
		return c(data); // do something else after executing c(data)
	}
};

And the questions are

  • how to execute a function (do_something()) after c(data) is executed?

  • Is it possible to write the code so that if do_something() does, in fact, nothing, the modified code generates (more or less) the same code as if the call to do_something() would not have been added at all?

Do something before return

For doing something before another statement, just call the function you want to execute before that other statement:

int bar(){
	do_something();
	return 42;
}

Do something after return in C

In C, this is extremely easy, mainly because

  • there are no exceptions

  • there are no user-defined destructors

  • there are no user-defined copy/move constructors

Instead of returning directly, store the result in a variable, do whatever you want to do after the return statement, and then return:

struct s{ /* ... */ };
struct s get_result();

struct s bar(){
	struct s result = get_result();
	do_something();
	return result;
}

if do_something is an empty function, then bar has the same behavior of

struct s baz(){
	return get_result();
}
Note 📝
in this and future examples, functions that do not return to the caller, like exit, abort or jumps, are considered out of scope.

Do something after return (ignoring exceptions)

To stay more similar to the C example let’s suppose that exceptions are disabled (with -fno-exception on GCC and clang), thus not this is not "true" C++ yet, but something outside of the standard.

In C++, if a variable is temporarily stored somewhere, it can generally be observed, as destructors and copy/move constructors can be customized.

Thus

struct s{
	s(const s&);
	s& operator=(const s&);
	~s();
};
s get_result();

void do_something();

s bar(){
	s result = get_result();
	do_something();
	return result;
}

s baz(){
	return get_result();
}

even if do_something() is an empty function, bar and baz might not be equivalent. In the case of bar, a call to ~s() might happen, while in baz not.

Fortunately, the compiler should be able to optimize the call to ~s() in bar away (and note that GCC does it even with optimizations disabled).

Named return value optimization is a valid optimization technique that can change the behavior of a conforming program, thus better not to rely on side effects in the destructor, copy, and assignment operators, if temporaries are involved.

I did not find a compiler flag like -fno-exception for MSVC.

Note that the same effect can be reproduced by declaring do_something as a noexcept function (and with some optimizations enabled), which is much better because this would be "true" C++, especially if do_something is indeed noexcept.

Deleted copy/move constructors

The previous example has, unfortunately, a catch.

Customized constructors and destructors are not an issue, but what if the copy and move constructors have been deleted?

In that case, even with NRVO, the compiler needs an (unused) copy or move constructor.

Thus the current approach does not cover all use cases, although it is not one that appears often.

Do something after return

If exceptions are involved, the most straightforward answer is to use a destructor.

struct s{
	s(const s&);
	s& operator=(const s&);
	~s();
};
s get_result();

void do_something();

s bar(){
	struct internal{
		~internal(){
			do_something();
		}
	} i;
	return get_result();
}

s get_result();

s baz(){
	return get_result();
}

The best part of this approach: we do not need to change the return statement, it will work with any type, moveable or not (void too!).

This issue is not specific to this example, but…​ what if do_something throws?

Destructors are noexcept by default, thus in this example, the code would lead to a call to terminate.

Changing the destructor of internal to noexcept(false) does not necessarily help. What if an exception is thrown from get_result and do_something throws too?

There can’t be two active exceptions, so this situation should be avoided.

Do something after return, but only on success

Success means that get_result does not throw.

This should be easy.

Ignoring the case of deleted copy and move constructors, the most straightforward implementation is:

struct s{
    s(const s&);
    s& operator=(const s&);
    ~s();
};
s get_result();

void do_something() ;

s bar(){
    s result = get_result();
    do_something();
    return result;
}

s baz(){
    return get_result();
}

It is the same code presented assuming no support for exception.

Otherwise, if one needs to support a non-moveable type, thanks to std::uncaught_exceptions, it is possible to write

#include <stdexcept>

struct s{
	s(const s&);
	s& operator=(const s&);
	~s();
};
s get_result();

void do_something();

s bar(){
	struct on_success{
		int num = std::uncaught_exceptions();
		~on_success(){
			if(std::uncaught_exceptions() == num){
				do_something();
			}
		}
	} i;
	return get_result();
}

s get_result();

s baz(){
	return get_result();
}

Note how the internal structure got more complicated.

A naive implementation might test if std::uncaught_exceptions() != 0, but it does not take into account that an exception could have been thrown before get_result() has been called, for example, if bar() is called inside a destructor, while the stack is unwinding because of an exception.

For this reason, it is recommended to use std::uncaught_exceptions() and not std::uncaught_exception(), which has been removed in C++20.

Do something after return, but only on failure

Failure means that get_result() throws an exception.

If we want to do something only if an exception has been thrown, the most readable approach is to catch the exception

struct s{
    s(const s&);
    s& operator=(const s&);
    ~s();
};
s get_result();

void do_something() ;

s bar(){
	try{
		return get_result();
	}catch(...){
		do_something();
		throw;
	}
}

s baz(){
	return get_result();
}

For completeness, it is possible to use a destructor, but I am not sure it provides an advantage, as the previous piece of code also supports types that cannot be copied and moved.

struct s{
	s(const s&);
	s& operator=(const s&);
	~s();
};
s get_result();

void do_something();

s bar(){
	struct on_failure{
		int num = std::uncaught_exceptions();
		~on_failure(){
			if(std::uncaught_exceptions() > num){
				do_something();
			}
		}
	} i;
	return get_result();
}

s get_result();

s baz(){
	return get_result();
}

Conclusion

There are two approaches: with and without the usage of a destructor.

Not using the destructor is less generic, it might involve some unnecessary moves or copies, but it is improbable if the function is simple enough. The main drawback is the lack of support for non-copyable and non-moveable types. If the action needs to be executed both when an exception is thrown or not, it will create some code duplication.

Using the destructor is the most generic approach, as it does not require a copy or move constructor. But it leads to more complicated code if the functions do not need to be executed when an exception is thrown, or only if an exception is thrown.

In those cases, one needs to call std::uncaught_exceptions() twice, which effectively depends on some global state, and thus will generally not be optimized away.

Performance considerations aside, there is little difference between the two approaches, use the one most readable.

If something needs to be executed always, even in case of an exception, use a destructor:

struct s{
	std::vector data;

	// do_something always executed after call to callback
	template <class callback>
	delctype(auto) fun(callback c) {
		// do something
		struct internal{
			~internal(){
				do_something();
			}
		} i;
		return c(data);
	}
};
Note 📝
evaluate if ~internal should be noexcept or not.

If something needs to be executed only if an exception has been thrown, catch the exception, and throw it again:

struct s{
	std::vector data;

	// do_something executed after call to callback on failure
	template <class callback>
	delctype(auto) fun(callback c) {
		// do something
		try{
			return c(data);
		} catch (...) {
			do_something();
			throw;
		}
	}
};
Note 📝
do_something() should probably never throw, consider making it noexcept

If something needs to be executed only in case of success, when an exception has not been thrown, and the return type is not moveable, use a destructor with std::uncaught_exceptions():

struct s{
	std::vector<int> data;

	// do_something executed after call to callback on success
	template <class callback>
	decltype(auto) fun(callback c)
	struct on_success{
		int num = std::uncaught_exceptions();
		~on_success(){
			if(std::uncaught_exceptions() == num){
				do_something();
			}
		}
	} i;
	return c(data);
};
Note 📝
~on_success should probably be noexcept(false).

Otherwise, copy the result to a temporary variable, and rely on NRVO:

struct s{
	std::vector<int> data;

	// do_something executed after call to callback on success
	// only copyable/moveable, non-reference, non-void types
	template <class callback>
	decltype(auto) fun(callback c)
	{
		auto r = c(data);
		do_something();
		return r;
		}
	}
};

But note that this code does not compile if callback returns void, and will not behave correctly if it returns a reference, thus it needs to be changed to

struct s{
	std::vector<int> data;

	// do_something executed after call to callback on success
	// no non-moveable types
	template <class callback>
	decltype(auto) fun(callback c)
	{
		using return_type = decltype(c(data));
		if constexpr( std::is_void_v<return_type> )
		{
			c(data);
			do_something();
		}
		else if constexpr( std::is_reference_v<return_type> )
		{
			auto&& r = c(data);
			do_something();
			return std::forward<decltype(r)>(r);
		}
		else
		{
			auto r = c(data);
			do_something();
			return r;
		}
	}
};

I do not like the version with the destructor, as it depends on std::uncaught_exceptions(), but it is arguably a simpler piece of code to maintain. The version without requires the use of some metaprogramming with std::invoke_result_t, constexpr if,

Thus, if something needs to be executed in case of success, the two guidelines together will give us the following function:

struct s{
	std::vector<int> data;

	// do_something executed after call to callback on success
	template <class callback>
	decltype(auto) fun(callback c)
	{
		using return_type = decltype(c(data));
		if constexpr( std::is_void_v<return_type> )
		{
			c(data);
			do_something();
		}
		else if constexpr( std::is_reference_v<return_type> )
		{
			auto&& r = c(data);
			do_something();
			return std::forward<decltype(r)>(r);
		}
		else if constexpr( not std::is_move_constructible<return_type>::value )
		{
			struct on_success{
				int num = std::uncaught_exceptions();
				~on_success(){
					if(std::uncaught_exceptions() == num){
						do_something();
					}
				}
			} i;
			return c(data);
		}
		else
		{
			auto r = c(data);
			do_something();
			return r;
		}
	}
};

Thus avoid, if possible, using std::uncaught_exceptions.

I am not too happy with the outcome (if one wants to do something only on success), the function is not trivial to write (even when leaving non-moveable types out), requires some metaprogramming, and relies on compiler optimizations. The language changed a lot, before constexpr if, writing that function would have been much harder. I wish the standard would mandate NRVO for such simple cases, removing the need to write a separate branch for such types and making it possible to store temporarily unmoveable types in variables too.

On the other hand, using std::uncaught_exceptions is not completely foolproof either (one needs to call it twice), has some possible overhead, and one needs to consider if the destructor of on_success should be marked noexcept(false). But it works naturally with all types and requires much less code.

Nevertheless, my recommended approach is to avoid the destructor for this constellation, and ignore not moveable types unless you must support them.

The complexity of this function is given by the fact it accepts a generic callback with any possible return value if there are more constraints (the return type of the callback is fixed), the code reduces greatly, and all metaprogramming techniques are not required.

If you do not have access to the current C++ standard, and thus have no access to constexpr if, you might prefer to use std::uncaught_exceptions, as it will be much simpler to maintain.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.