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

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

  • 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

  • I'm eagerly awaiting posts on the compiler back-end improvements :-)

  • Sorry to have taken so long to reply. Really appreciate your answer, too.

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

    I'll try to explain what I mean. Perhaps I'm just confused about how this stuff works, so anyone else reading please don't assume any of this is correct!

    Say you have:

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

    You might call that with two strings, but where the first string is an lvalue and the second string is an rvalue.

    In that case "T&& t1" and "T&& t2" have different (meta-)types.

    i.e. "T&&" means two different things within the same function body and its meaning can only be resolved in combination with a variable name. I assumed that's why there was no zero-argument thing like arg<T>().

    Or would that call simply not compile? (That sounds like what you said in your reply, but I'm not certain we're talking about the same thing.) Do the two T&& arguments have to have the same l/rvalue-ness, and you have to use a new template typename if you want to allow them to be different?

    If it wouldn't compile, does that mean there's no way to have a function where the two arguments must be the same type as each other, but may have different l/rvalue-ness?

    Either way it seems fine, on the face of it. I'm not criticising the design; just trying to understand exactly how things will work.

    Thanks, again, for your time!

  • [Leo Davidson]

    > Say you have:

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

    This is broken. (You can write it, but it won't be useful.) If you want perfect forwarding, you need to take T1&&, T2&&, T3&&, etc.

    > You might call that with two strings, but where the first string is an lvalue and the second string is an rvalue.

    > In that case "T&& t1" and "T&& t2" have different (meta-)types.

    > i.e. "T&&" means two different things within the same function body and its meaning can only be resolved

    > in combination with a variable name.

    This is incorrect. C++ (both 98/03 and 0x) doesn't work like this.

    In C++98/03, if I have template <typename C> void foo(const C& c1, const C& c2), and I call foo(v, l) with a vector<int> v and list<int> l, will this compile? No, because template argument deduction will fail. When you call foo(v, l), the compiler tries to figure out what C should be. The arguments are telling it that C should be vector<int> and list<int> simultaneously, which is impossible. (I am deliberately omitting other details of template argument deduction and overload resolution here.)

    C++0x works identically. Given template <typename T> void bar(T&& t1, T&& t2) and bar(expression1, expression2), either expression1 and expression2 cause the same T to be deduced (in which case compilation succeeds), or they don't (in which case compilation fails).

    C++0x adds a couple of twists to template argument deduction (specifically, when matching the function parameter T&& t against the function argument X x where x is an lvalue, T is deduced to be X& instead of plain X), substitution (specifically, reference collapsing, where you start with T&&, substitute X& for T which generates X& &&, which then collapses to X&), and overload resolution, but otherwise everything works like C++98/03. Within a single instantiation, a single type like T&& never means two different things. It can mean a single slightly surprising thing (for example, X&), but only one.

    > Or would that call simply not compile?

    Bingo. VC actually generates a good compiler error for this:

    C:\Temp>type meow.cpp

    #include <iostream>

    #include <ostream>

    #include <string>

    using namespace std;

    template <typename T> void meow(T&& t1, T&& t2) {

       cout << t1 << endl;

       cout << t2 << endl;

    }

    string rv() {

       return "kittens";

    }

    int main() {

       string lv("fluffy");

       meow(lv, rv());

    }

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

    meow.cpp

    meow.cpp(18) : error C2782: 'void meow(T &&,T &&)' : template parameter 'T' is ambiguous

           meow.cpp(6) : see declaration of 'meow'

           could be 'std::string'

           or       'std::string &'

    > (That sounds like what you said in your reply, but I'm not certain we're talking about the same thing.)

    > Do the two T&& arguments have to have the same l/rvalue-ness, and you have to use a new template typename

    > if you want to allow them to be different?

    Yes.

    > If it wouldn't compile, does that mean there's no way to have a function where the two arguments

    > must be the same type as each other, but may have different l/rvalue-ness?

    There is a way: static_assert.

    C:\Temp>type purr.cpp

    #include <iostream>

    #include <ostream>

    #include <string>

    #include <type_traits>

    using namespace std;

    template <typename T, typename U> void purr(T&& t, U&& u) {

       static_assert(

           is_same<

               typename remove_reference<T>::type,

               typename remove_reference<U>::type

           >::value,

           "purr(t, u) requires t and u to have identical types, "

           "but they can have different lvalueness/rvalueness.");

       cout << t << endl;

       cout << u << endl;

    }

    string rv() {

       return "kittens";

    }

    int main() {

       string lv("fluffy");

       purr(lv, rv());

    }

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

    purr.cpp

    C:\Temp>purr

    fluffy

    kittens

    Attempting to call purr(lv, 1729); fails:

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

    purr.cpp

    purr.cpp(14) : error C2338: purr(t, u) requires t and u to have identical types, but they can have different lvalueness/rvalueness.

           purr.cpp(27) : see reference to function template instantiation 'void purr<std::string&,int>(T,U &&)' being compiled

           with

           [

               T=std::string &,

               U=int

           ]

    (I'm not bothering with constness here. You can also use SFINAE to avoid a hard error.)

    > Either way it seems fine, on the face of it. I'm not criticising the design; just trying to understand exactly how things will work.

    Sorry for the confusion. (I myself was confused when you started talking about a construct, foo(T&&, T&&), which I hadn't presented in my posts.)

    I hope things make sense now.

  • Huge thanks! That explained things perfectly and has cleared up what I had misunderstood.

    I enjoyed the continuation of the feline theme, too. :)

  • Hey guys,

    So is this going to be in Beta1? and when is that supposed to be released? I was hoping tech-Ed, but  AFAIK, no beta 1 are being haded out.

  • [Jusendo]

    > So is this going to be in Beta1?

    Yes. VC10 Beta 1 will contain lambdas, auto, static_assert, rvalue references, decltype, and our updated Standard C++ Library implementation.

    > and when is that supposed to be released?

    Magic 8 Ball says: Better not tell you now.

  • Visual Studio 2010 Beta 1 introduces a number of exciting new features for the C++ developer as we include

  • I installed beta1 & create a native console project to try new features.

    auto: works.

    decltype, lambda: doesn't work. I copied the code here & let IDE to compile (not from command line), but failed.

    Do I foget to do to some configuration?

  • [Ge yong]

    > decltype, lambda: doesn't work. I copied the code

    > here & let IDE to compile (not from command line),

    > but failed.

    "doesn't work" and "failed" are not specific enough for my psychic debugging powers. Can you at least show me the compiler errors?

  • I find the reason why it seems to "doesn't work".

    The code:

    #include "stdafx.h"

    #include <algorithm>

    #include <iostream>

    #include <ostream>

    #include <vector>

    using namespace std;

    int main() {

       int *p= nullptr;

       vector<int> v;

       for (int i = 0; i < 10; ++i) {

           v.push_back(i);

       }

       for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });

       cout << endl;

    }

    In output window, no problem:

    1>f:\temp\tconsole\tconsole\tconsole.cpp(13): error C2065: 'nullptr' : undeclared identifier

    unfortunately, what I see first is Error List window, the messages in it are confusing:

    Error 1 IntelliSense: identifier "nullptr" is undefined f:\temp\tconsole\tconsole\tconsole.cpp 13 10 TConsole

    Error 2 IntelliSense: expected an expression f:\temp\tconsole\tconsole\tconsole.cpp 21 34 TConsole

    Error 3 error C2065: 'nullptr' : undeclared identifier f:\temp\tconsole\tconsole\tconsole.cpp 13 1 TConsole

    When I clicked on "Error 2", the character '[' is highlighted on line 21 (the line started with for_each). This make me think "lambda doesn't work".

    by the way, I see "nullptr" become blue, color of keyword. How to make it work?

  • Thanks for the clarification.

    VC10 Beta 1 doesn't support nullptr, and we currently have no plan to add it to VC10.

    VC10 Beta 1 Intellisense doesn't support the C++0x core language features that the actual compiler supports. This will obviously change.

    It appears that VC10 Beta 1 Intellisense recognizes some C++0x keywords as such, but that doesn't mean that it recognizes the actual features.

  • Thanks for posting these detailed explanations of new X++0x features.  Very informative and interesting stuff, even if it sometimes hurts my head a bit!

    Much of the C++0x features (at least what I call the "sexy" features) seem to be similar to those of C# : auto(var), lambdas, initializer lists, enum class, and Range-for.  I suppose these types of features are so compelling it's hard to ignore them.  This is good news for devs programming with both languages, like myself (practically every time I open VC++ I wish I could use C# foreach, lambdas and enum classes).

    Very exciting stuff!

Page 2 of 2 (27 items) 12