Deducing return types in C++
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>
decltype(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 an 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>()))
won’t 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 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>
decltype(auto) lock( const mutexed_obj<T, mutex>& mo, F f ) {
std::lock_guard l{ mo.m };
return f( mo.obj );
}
template <class F>
decltype(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 to declare 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, but at that point, not specifying the return type is even simpler.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.