decltype: C++0x Features in VC10, Part 3

decltype: C++0x Features in VC10, Part 3

Rate This
  • Comments 27

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 <string>

#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 abomination.)

 

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_n(n) { }

    int get() const { return m_n; }

private:

    int m_n;

};

 

Joules operator*(const Watts& w, const Seconds& s) {

    return Joules(w.get() * s.get());

}

 

int main() {

    vector<Watts> w;

    w.push_back(Watts(2));

    w.push_back(Watts(3));

    w.push_back(Watts(4));

 

    vector<Seconds> s;

    s.push_back(Seconds(5));

    s.push_back(Seconds(6));

    s.push_back(Seconds(7));

 

    vector<Joules> j;

 

    transform(w.begin(), w.end(), s.begin(), back_inserter(j), Multiplies());

 

    for_each(j.begin(), j.end(), [](const Joules& r) { cout << r.get() << endl; });

}

 

C:\Temp>cl /EHsc /nologo /W4 mult.cpp

mult.cpp

 

C:\Temp>mult

10

18

28

 

You might ask, "is all of this generality really necessary?"  The answer is yes, yes it is.  I've already mentioned how perfect forwarding and decltype make arithmetic operation functors easier to use (by removing the need to repeat element types), more flexible (by dealing with mixed argument and return types), and more efficient (by respecting move semantics).  Essentially, perfect forwarding and decltype allow you to write more "transparent" code.  Inflexible code and inefficient code are not transparent - their presence can't be ignored.

 

 

advanced rules

 

decltype is powered by several rules.  However, if you stick to the pattern above, they don't matter and it just works.  I rarely get to say that about C++, but it's true in this case.

 

Although the vast majority of decltype uses will follow the pattern above, decltype can be used in other contexts.  In that case, you've activated expert mode, and you should read the rules in their entirety.  In the C++0x Working Draft N2857, they're given by 7.1.6.2 [dcl.type.simple]/4.

 

 

but wait, there's more

 

decltype is the fifth and final C++0x Core Language feature being added to VC10.  While it wasn't in the VC10 CTP, it's in VC10 Beta 1.  Also in VC10 Beta 1 are many C++0x Standard Library features, which I'll be blogging about soon!

 

Stephan T. Lavavej

Visual C++ Libraries Developer

  • PingBack from http://microsoft-sharepoint.simplynetdev.com/decltype-c0x-features-in-vc10-part-3/

  • Would decltype(Forward<T>(T()) + Forward<U>(U())) work as a preceding return type?

  • No, because T and U might not have default constructors.

  • Couldn't there be a default for the auto keyword in this case where the return type of the returned expression is used? Instead of having to essentially repeat the code.

    Something like this :

    template <typename T, typename U>

    auto operator()(T&& t, U&& u) const -> decltype(__default__)

    {

    return forward<T>(t) * forward<U>(u);

       }

  • That was considered, but dropped between N1607 (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1607.pdf) and N1705 (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1705.pdf) due to insufficient support from members of the Evolution Working Group. See N2869 (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2869.html) for the whole sequence of papers from decltype's evolution.

  • This is a very long comment so I've split it into four parts: Two question parts, then some thoughts / rambling, and finally a thank-you. :-)

    ** Null-reference trick **

    "Technically, decltype(forward<T>(*static_cast<T *>(0)) + forward<U>(*static_cast<U *>(0))) could go on the left, but that's an abomination."

    I agree that is ugly.

    When you say you could use that trick to avoid naming particular variables, you mean in that specific example and not in general, right?

    As I understand things, the trick doesn't preserve the lvalue/rvalue-ness of the particular variables, which could be important in determining the return type, so it won't always work. (It works in this example because the lvalue/rvalue-ness isn't important, assuming a typical operator*.)

    OTOH, if you could always use the trick then you could have something like std::forward which does it and hides the ugliness.

    ** What if the variable names vary? **

    Looking at the examples in this post again makes me wonder, what if the return statement doesn't always use the same variables? For example, what goes in place of "t???" here:

    template <typename T>

    auto operator()(T&& t1, T&& t2) const

    -> decltype(forward<T>(t???) * forward<T>(t???))

    {

       if (t1 > t2)

       {

           return forward<T>(t1) * forward<T>(t1);

       }

       else

       {

           return forward<T>(t2) * forward<T>(t2);

       }

    }

    Presumably, if the code will compile at all, then it doesn't matter which permutation of t1 and t2 you use to fill in the two t???. And if the lvalue/rvalue-ness of t1 and t2 are not the same, and the operator* is such that it matters, then the code won't compile because the two return statements will have different types. (Or the return statements' results will be coerced into whatever type the decltype specifies.)

    Am I thinking along the right lines, or completely confused?

    ** Thoughts **

    It's quite odd that two "T&&" in the same context can be different despite being written exactly the same. It's like we have hidden meta-types now. :-) Very useful though, when needed, and something that most people should be able to ignore most of the time.

    I don't mind that the equation stating the return type goes on the right. (That's consistent with the lambda stuff and presumably dictated by parsing/syntax issues squeezing these features into the existing language.) And I don't mind that we have to explicitly state the return type/equation. (Fair enough.)

    I feel for anyone trying to learn C++ but I would not argue against any of these additions as they solve real problems in a way which makes (non-library) code easier to read and write. Anyone learning C++ needs to realise that the aim is not to hold the entire language in your head. Almost nobody does that. There are some features you should be aware of, but not expert in, where you can re-read the relevant chapter/webpage in the rare cases you need them.

    It must be a bit more daunting now than it was before, but that's life. IMO, C++ has always been a language that it takes years to master, and where mastery cannot come from a book or even from mental capacity alone. You have to trip over all of the caveats with your own foot before you truly understand them. It can be horrible for beginners but it's still probably my favourite language.

    My main complaints about C++ could never be fixed without breaking existing code. (e.g. I hate the fact that you can get default constructors/operators without explicitly asking for them. Anything where the default may be catastrophically wrong should be opt-in, not opt-out, IMO. But it's too late to change that now.)

    ** Thanks! **

    By the way, I read all three parts of this series over the last two days and I want to say thanks for describing things so well and thinking of examples that strike the right balance between complexity and abstraction*. Only the section on forwarding took me a few reads to grasp and I guess that is inherently complex and difficult to explain. I think you did a great job.

    (*That is to say, often when language features are described the examples are so abstract that I have to almost compile them in my head to work out what's being demonstrated. Or the examples are so complex, with so many unimportant side details, that it takes time to dig into the important bits. Your examples, OTOH, tended to be clear just from skimming the code. Nice one!)

  • My latest in a series of the weekly, or more often, summary of interesting links I come across related to Visual Studio. US ISV Developer Evangelism Team posted some links to money saving offers for ISVs when purchasing or upgrading Visual Studio or MSDN

  • "What if the variable names vary?"

    Since return type of a function cannot vary depending on the input data, then the operator* should resolve to the same regardless of the order that t1 and t2 are multiplied in. So it shouldn't matter.

  • @Nicol Bolas:

    "then the operator* should resolve to the same regardless of the order"

    The same what, is my question. The two return statements are presumably coerced into the type of the decltype statement, and there's an error if that isn't possible. That's my guess anyway, but I was wondering if the guess is correct.

    That is, the return type cannot vary but the type of each return statement can vary, so long as it can be turned into the return type.

    A bit like this (which compiles) but where the lvalue/rvalue-ness of the types varies rather than the types themselves.

    double test(long a, char b)

    {

    if (a > b)

    {

    return a * a;

    }

    else

    {

    return b * b;

    }

    }

  • [Leo Davidson]

    > When you say you could use that trick to avoid naming particular variables, you mean in that specific example and not in general, right?

    It works in general, so trailing return types weren't an absolutely necessary feature. However, programmer sanity is important, which is why trailing return types were added.

    > As I understand things, the trick doesn't preserve the lvalue/rvalue-ness of the particular variables

    That information is contained within the deduced types T and U, and preserved by forward<T>(stuff) and forward<U>(stuff) regardless of what you give them (but you have to give them something).

    Something like arg<T>() could have been developed, taking nothing and returning T&& to a dereferenced null pointer (this function would be lethal to ever call - but decltype does not evaluate its expression). But instead of putting the return type on the left and teaching people to translate forward<T>(t) into arg<T>() there, putting the return type on the right and teaching people "give decltype exactly what you're going to return" is easier.

    > (It works in this example because the lvalue/rvalue-ness isn't important, assuming a typical operator*.)

    Although lvalueness/rvalueness doesn't generally affect operator+() and operator*(), my functors have been carefully written to respect them, including in the decltype-powered return type.  Saying decltype(t + u) would not respect lvalueness/rvalueness.

    > What if the variable names vary?

    Then you need to figure out some expression that has the correct type.  The function can only have one return type, after all.

    C++0x <type_traits> provides common_type which may be of use here.

    In your case, you're taking two T&&, so forward<T>(t1) * forward<T>(t1) and forward<T>(t2) * forward<T>(t2) are guaranteed to have the exact same type (same inputs, same output).  Note that taking two T&& doesn't produce perfect forwarding, and will probably trigger template argument deduction failure in many cases (where the deduced Ts differ).

    > It's quite odd that two "T&&" in the same context can be different despite being written exactly the same.

    I'm not sure what you're referring to.

    In my examples, I use operator()(T&& t, U&& u).  This allows the arguments to have different types.

    A different issue is that in one instantiation, T&& might actually be string&&, while in another instantiation, it might be int&.  This is covered in Part 2, reference collapsing, and is otherwise not fundamentally different from how in C++98/03, X& might actually be vector<int>& in one instantiation and const list<int>& in other (where X is deduced to be const list<int>).

    > I feel for anyone trying to learn C++ but I would not argue

    > against any of these additions as they solve real problems

    > in a way which makes (non-library) code easier to read and write.

    Excellently put.

    Also, many C++0x features are simpler than the workarounds you'd have to use in C++98/03 (if they were possible at all).  Which can make C++0x easier to learn from scratch (whereas we C++98/03 programmers had to learn the old ways, and now we have to learn the new ways on top of that).

    > By the way, I read all three parts of this series over the last two

    > days and I want to say thanks for describing things so well and

    > thinking of examples that strike the right balance between complexity and abstraction*.

    You're welcome! Yes, coming up with simple but plausible examples is tricky.

  • Will VS C++ 2010 deliver real SSE3/SSE4 support (compiler optimization /arch:SSE3 ) instead of just intrinsics?

  • Ok, I don't get one thing, after all these post, who can be so obsessed with c++ to come up with all these?

    The reason the language is still alive today ( next to java/c#/php/... which have greater momentum, easier syntax, extendablity, etc.), because the programmer can more or less tell or check in the .asm output how it will compile to assembly and yet he does not have to write it in assembly! Anything that shifts our beloved close-to-the-metal c/c++ towards the mentioned languages don't have much "market value" because that area is already full of competition.

    I say just keep improving the code generator, add sse/avx/larrabee support, smarter intrinsic translationm, and of course less ICE :P

  • We are working on all of that too !

    In the coming months, we will share details about the codegen optimizations that will be part of VC10.

    BTW, ICEs are quite rare now.  If you find some, please report them back to us!

  • Gabest:

    Please note that these features follow C++'s spirit of staying close to the metal while providing modern abstractions.

    * Lambdas compile into function objects which benefit from inlining. This is superior to library machinery (like bind() and mem_fn()) which is complicated enough to defeat the inliner.

    * auto, static_assert, and decltype are purely compile-time features. They impose no overheads on generated code.

    * Rvalue references automatically replace unnecessary dynamic memory allocations with instantaneous pointer twiddling. C++0x mostly eliminates C++98's biggest overhead compared to C, its tendency to perform unnecessary copies.

    The compiler is divided into a "front-end" (FE) and "back-end" (BE). Summarizing how they work, the FE parses C++ while the BE generates assembly. These C++0x Core Language features are FE features, unrelated to BE features like code generation, intrinsics, and so forth. FE optimizations like rvalue references and BE optimizations like smarter code generation happily coexist with each other. Also, completely different developers work on the FE and BE, so you don't have to worry that time spent implementing lambdas is somehow taking time away from improving code generation, because it's not.

  • To answer a comment above: Wouldn't it be nice to deduce the return type for 'simple' functions, in just the same way that lambda is required to?

    template< typename T, typename U >

    auto plus( T t, U u ) {

     return t + u;

    }

    As Steven says, this was considered early in the C++0x cycle, and ultimately rejected.  However, it may yet return as part of the last ongoing piece of work on this part of the standard, see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2825.html

    Now I don't want to talk up the chances of success at this point!  The paper proposes chaning the auto keyword (in new function declarations) to [] so that functions and lambda have a similar syntax.  Essentially, anything that is 'callable' is introduced by square brackets, and there is no difference between a regular function, and a lambda expression with an empty capture list (meaning you can use lambda for callbacks into many Windows APIs!)

    The problem is that many consider this syntax ugly, and we will shortly have two popular compilers shipping with the auto syntax.

    So the idea is not dead yet, but will probably be decided one way or the other at the next standards meeting in July.

Page 1 of 2 (27 items) 12