Part 1 of this series covered lambdas, auto, and static_assert.
Part 2 of this series covered rvalue references, which enable move semantics and perfect forwarding.
Today, I’m going to talk about decltype, which allows perfect forwarding functions to have arbitrary return types. It’s of interest to people who are writing highly generic code.
the return type problem
C++98/03 has an interesting blind spot – given an expression like x * y, where x and y have arbitrary types, there’s no way to say “the type of x * y“. If x is of type Watts and y is of type Seconds, then x * y might be of type Joules. Given print(const T& t), you can call print(x * y), and T will be deduced to be Joules, but this doesn’t work in reverse: when writing multiply(const A& a, const B& b), you can’t name its return type while preserving full generality. Even though when multiply<A, B>() is instantiated, the compiler knows the type of x * y, that information is unavailable to you here. The C++0x keyword decltype removes this blind spot, allowing you to say “multiply() returns the type of x * y“. (decltype is an abbreviation of “declared type”; I pronounce it as rhyming with “speckle type”.)
decltype: the pattern
Here’s how to write a completely generic functor that wraps operator+(). This Plus functor is not a template, but it has a templated function call operator that takes two arguments of arbitrary (and possibly different) types, adds them together, and returns the result, which can be of arbitrary (and possibly different from both of the arguments) type.
C:Temp>type plus.cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <ostream>
#include <s tring>
#include <utility>
#include <vector>
using namespace std;
struct Plus {
template <typename T, typename U>
auto operator()(T&& t, U&& u) const
-> decltype(forward<T>(t) + forward<U>(u)) {
return forward<T>(t) + forward<U>(u);
}
};
int main() {
vector<int> i;
i.push_back(1);
i.push_back(2);
i.push_back(3);
vector<int> j;
j.push_back(40);
j.push_back(50);
j.push_back(60);
vector<int> k;
vector<string> s;
s.push_back(“cut”);
s.push_back(“flu”);
s.push_back(“kit”);
vector<string> t;
t.push_back(“e”);
t.push_back(“ffy”);
t.push_back(“tens”);
vector<string> u;
transform(i.begin(), i.end(), j.begin(), back_inserter(k), Plus());
transform(s.begin(), s.end(), t.begin(), back_inserter(u), Plus());
for_each(k.begin(), k.end(), [](int n) { cout << n << ” “; });
cout << endl;
for_each(u.begin(), u.end(), [](const string& r) { cout << r << ” “; });
cout << endl;
}
C:Temp>cl /EHsc /nologo /W4 plus.cpp
plus.cpp
C:Temp>plus
41 52 63
cute fluffy kittens
Compare this to C++98/03 <functional>’s std::plus<T> (which is unchanged in C++0x). Because it’s a class template, you’d have to pass plus<int>() and plus<string>(), repeating the element types. Its non-templated function call operator has the form T operator()(const T& x, const T& y) const, making it unable to deal with 2 different types, much less 3 different types, without resorting to implicit conversions. (You can feed plus<string>() a string and a const char *. That will construct a temporary string from the second argument, before concatenating the two strings. The performance of this is not especially desirable.) Finally, because it takes const T&, it can’t take advantage of C++0x move semantics. Plus avoids all of this: Plus() doesn’t repeat the element type, it deals with the “3 different types” case, and because it uses perfect forwarding, it respects move semantics.
trailing return types
Now, let’s look at that templated function call operator again:
template <typename T, typename U>
auto operator()(T&& t, U&& u) const
-> decltype(forward<T>(t) + forward<U>(u)) {
return forward<T>(t) + forward<U>(u);
}
Here, auto has a different meaning from for (auto i = v.begin(); i != v.end(); ++i), where it says “make the type of this thing the same as the type of whatever initializes it”. When used as a return type, auto says “this function has a trailing-return-type; after I declare its parameters, I’ll tell you what its return type is”. (The C++0x Working Draft N2857 calls this a late-specified return type, but this is being renamed to trailing-return-type; see paper N2859.) If this seems suspiciously similar to how lambdas are given explicit return types, that’s because it is. A lambda’s return type has to go on the right in order for its lambda-introducer [] to appear first. Here, the decltype-powered return type has to go on the right in order for the function parameters t and u to be declared first. Where the auto appears on the left, the template parameters T and U are visible, but the function parameters t and u are not yet visible, and that’s what decltype needs. (Technically, decltype(forward<T>(*static_cast<T *>(0)) + forward<U>(*static_cast<U *>(0))) could go on the left, but that’s an abomina tion.)
As for the expression given to decltype, giving it the same expression as the return statement ensures correctness in all cases. (Pop quiz: why would decltype(t + u) be wrong?) The repetition here is unavoidable but centralized – it appears exactly once, on adjacent lines, so it is not dangerous.
another example
For completeness, here’s that “3 different types” example:
C:Temp>type mult.cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <ostream>
#include <utility>
#include <vector>
using namespace std;
struct Multiplies {
template <typename T, typename U>
auto operator()(T&& t, U&& u) const
-> decltype(forward<T>(t) * forward<U>(u)) {
return forward<T>(t) * forward<U>(u);
}
};
class Watts {
public:
explicit Watts(const int n) : m_n(n) { }
int get() const { return m_n; }
private:
int m_n;
};
class Seconds {
public:
explicit Seconds(const int n) : m_n(n) { }
int get() const { return m_n; }
private:
int m_n;
};
class Joules {
public:
explicit Joules(const int n) : m_
0 comments