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

Deducing return types in C++


5 - 6 minutes read, 1205 words
Categories: c++
Keywords: c++ meta-programming

declval and typename are two utilities introduced in C++11 that extend that the meta-programming capabilities.

The main area where I have been using them is for declaring return types.

Since C++14, their usage, thanks to auto and additional deduction guides, is in many cases not necessary. Thus the main use-cases I had for those utilities is for projects that need or want to be C++11-compatible.

A simple example for introducing decltype could be something like

template <class T>
auto add(const T& lhs, const T& rhs) -> decltype(lhs + rhs) {
	return lhs + rhs;
}

decltype "returns" the type of the expressions between parenthesis.

Since C++14 the code can be simplified to

template <class T>
auto add(const T& lhs, const T& rhs) {
	return lhs + rhs;
}

It might not seem a big deal, but specifying the return type is error-prone, and not always intuitive.

as_span

While std::span is a C++17 library addition, it is possible to backport it with ease for older C++ standards.

One useful piece of code, especially when adding it to existing code-basis, is adding helper functions for converting existing and possibly custom containers to span.

This is the first C++11 implementation I came up

#include <span>

namespace fek {
	template <class T>
	struct data_traits {
		using element_type = typename std::pointer_traits<decltype(std::data(std::declval<T>()))>::element_type;
	};

	template <class C>
	auto as_span(C& c) -> std::span<typename data_traits<C&>::element_type> {
		return std::span<typename data_traits<C&>::element_type>(std::data(c), std::size(c));
	}
	template <class C>
	auto as_span(const C& c) -> std::span<typename data_traits<const C&>::element_type> {
		return std::span<typename data_traits<const C&>::element_type>(std::data(c), std::size(c));
	}
}

In this case, data_traits is a helper structure for getting the dereferenced return type of std::data. std::declval<T> is used for "creating" an object of type T. Depending on T, Writing T() might not be valid, as T might not have a default constructor. std::declval overcomes this limitation.

Note that std::declval can only be used in unevaluated contexts, thus it cannot be used for creating real objects and bypassing the constructor.

decltype is used for getting the return type from std::data. On the return type, std::pointer_traits, one of the many utilities provided by the standard library, is used for getting the element type pointed to (writing *decltype(std::data(std::declval<T>())) wont work).

The result is aliased in element_type inside the structure data_traits. This makes it possible to reuse data_traits<T>::element_type, in this case for both as_span functions.

With C++14, a possible implementation would be

#include <span>

namespace fek {
	template <class C>
	auto as_span(C& c) {
		auto p = std::data(c);
		return std::span<typename std::pointer_traits<decltype(p)>::element_type>(p, std::size(c));
	}
	template <class C>
	auto as_span(const C& c) {
		auto p = std::data(c);
		return std::span<typename std::pointer_traits<decltype(p)>::element_type>(p, std::size(c));
	}
}

But in this case, there is an even simpler implementation that does not require decltype at all; by adding a layer of indirection,

In C++14 the implementation would be

#include <span>

namespace fek {
	template <class P>
	auto as_span_impl(P* p, std::size_t s) {
		return std::span<P>(p, s);
	}

	template <class C>
	auto as_span(C& c) {
		return as_span_impl(std::data(c), std::size(c));
	}
	template <class C>
	auto as_span(const C& c) {
		return as_span_impl(std::data(c), std::size(c));
	}
}

Note that while specifying the return type of as_span_impl(P* p, std::size_t s) is easy, as it would be

template <class P>
auto as_span_impl(P* p, std::size_t s) -> std::span<P> {
    return std::span<P>(p, s);
}

This new function makes it much easier to specify the return type of the of as_span function too, even in C++11:

template <class C>
auto as_span(C& c) -> decltype(as_span_impl(std::data(c), 0)) {
	return as_span_impl(std::data(c), std::size(c));
}
template <class C>
auto as_span(const C& c) -> decltype(as_span_impl(std::data(c), 0)) {
	return as_span_impl(std::data(c), std::size(c));
}

mutexed_obj

The lock function of mutexed_obj is another example where the possibility to use C++14 simplifies the declaration of a function.

In C++14 one possible declaration and implementation of the template lock function is

template <class F>
auto lock( const mutexed_obj<T, mutex>& mo, F f ) {
	std::lock_guard l{ mo.m };
	return f( mo.obj );
}
template <class F>
auto lock( mutexed_obj<T, mutex>& mo, F f ) {
	std::lock_guard l{ mo.m };
	return f( mo.obj );
}

in C++11, a first attempt would be

template <class F>
auto lock( const mutexed_obj<T, mutex>& mo, F f ) -> decltype(f(mo.obj)){
	std::lock_guard l{ mo.m };
	return f( mo.obj );
}
template <class F>
auto lock( mutexed_obj<T, mutex>& mo, F f ) -> decltype(f(mo.obj)){
	std::lock_guard l{ mo.m };
	return f( mo.obj );
}

Unfortunately, this code will not work with forward-declared types, as decltype needs a fully specified type. In such situations, one needs to use something else for declaring the return type. In this case std::result_of (deprecated in C++17 in favour of std::invoke_result) seems the right tool:

template <class F>
auto lock( const mutexed_obj<T, mutex>& mo, F f ) -> typename std::result_of<F&&(const T&)>::type {
	std::lock_guard l( mo.m );
	return f( mo.obj );
}
template <class F>
auto lock( mutexed_obj<T, mutex>& mo, F f ) -> typename std::result_of<F&&(T&)>::type {
	std::lock_guard l( mo.m );
	return f( mo.obj );
}

This declaration should be good enough for most use-cases. The drawbacks of std::result_of_t<F(Args…​)> are

  • arguments that are arrays or function types decay to pointers

  • neither F nor any argument can be an abstract class type

  • top-level cv-qualifier of any arguments are discarded

Using references (thus std::result_of<F&&(const T&)> and std::result_of<F&&(T&)>) overcomes those limitations. std::invoke_result does not have those shortcomings, if one has access to C++17.


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

You can contact me here.