The Concurrency Runtime and Visual C++ 2010: The decltype Type Specifier
Welcome back! Last time, we looked at how to use the auto keyword in Visual C++ 2010 to implicitly declare local variables based on their initialization (see The Concurrency Runtime and Visual C++ 2010: Automatic Type Deduction). This week we’ll look at yet another great Visual C++ 2010 language feature that you can use when you write code that uses the Concurrency Runtime: decltype.
Imagine that you are writing reusable dataflow components that use the Asynchronous Agents Library. One of these components might be a message-block type that takes two addends and returns their sum. You can create a function, let’s call it make_dataflow_adder, that creates a Concurrency::transformer object. This transformer object takes as input the addends (as a std::tuple object) and produces their sum as output:
1: template<typename T1, typename T2>
2: transformer<tuple<T1, T2>, UNKNOWNtype>* make_dataflow_adder()
3: {
4: return new transformer<tuple<T1, T2>, UNKNOWNtype>(
5: [](tuple<T1, T2> addends) { return get<0>(addends) + get<1>(addends); }
6: );
7: }
In this example, UNKNOWNtype remains as a placeholder. What should this type be, T1, T2, or something else? operator+ for objects of type T1 and T2 does not always produce a result of type T1 or T2. For instance, the following example shows a case where operator+ for objects of type A and B produces a result of type int:
1: struct A {
2: int Value;
3: };
4:
5: struct B {
6: int Value;
7: };
8:
9: int operator+(const A& a, const B& b)
10: {
11: return a.Value + b.Value;
12: }
One way to resolve UNKNOWNtype might be to add a third template argument to the make_dataflow_adder function, say TResult, and force the user to provide the result type. Aside from being somewhat of an inconvenience, what if your user is extending your library and does not know what type to provide?
Visual C++ 2010 provides the decltype keyword as a great way to solve this. Simply put, the decltype keyword provides the type of the provided expression. When you use the decltype keyword to rewrite the make_dataflow_adder function, you eliminate the need for a third template type parameter and create code that is more usable:
1: template<typename T1, typename T2>
2: transformer<tuple<T1, T2>, decltype(T1() + T2())>* make_dataflow_adder()
3: {
4: return new transformer<tuple<T1, T2>, decltype(T1() + T2())>(
5: [](tuple<T1, T2> addends) { return get<0>(addends) + get<1>(addends); }
6: );
7: }
The following shows a complete example that uses the revised make_dataflow_adder function:
1: // dataflow-adder.cpp
2: // compile with: /EHsc
3: #include <agents.h>
4: #include <complex>
5: #include <tuple>
6: #include <iostream>
7: #include <memory>
8:
9: using namespace Concurrency;
10: using namespace std;
11:
12: // Utility function that creates a unique_ptr object from the
13: // specified native pointer.
14: template <typename T>
15: unique_ptr<T> make_unique_ptr(T* ptr)
16: {
17: return unique_ptr<T>(ptr);
18: }
19:
20: // Creates a transformer object that takes as input two addends (as a tuple)
21: // and produces their sum as output.
22: template<typename T1, typename T2>
23: transformer<tuple<T1, T2>, decltype(T1() + T2())>* make_dataflow_adder()
24: {
25: return new transformer<tuple<T1, T2>, decltype(T1() + T2())>(
26: [](tuple<T1, T2> addends) { return get<0>(addends) + get<1>(addends); }
27: );
28: }
29:
30: // An example structure that wraps an int value.
31: struct A
32: {
33: A() : Value(0) {}
34: A(int value) : Value(value) {}
35: int Value;
36: };
37:
38: // An example structure that wraps an int value.
39: struct B
40: {
41: B() : Value(0) {}
42: B(int value) : Value(value) {}
43: int Value;
44: };
45:
46: // Sums an A object and a B object to produce an int result.
47: int operator+(const A& a, const B& b)
48: {
49: return a.Value + b.Value;
50: }
51:
52: int wmain()
53: {
54: // Use the make_dataflow_adder function to create several transformer
55: // objects that add different kinds of types.
56:
57: auto t1 = make_unique_ptr(make_dataflow_adder<int, int>());
58: auto t2 = make_unique_ptr(make_dataflow_adder<wstring, wstring>());
59: auto t3 = make_unique_ptr(make_dataflow_adder<complex<double>, complex<double>>());
60: auto t4 = make_unique_ptr(make_dataflow_adder<A, B>());
61:
62: // Send to and receive from the transformer objects to illustrate
63: // their behaviors.
64:
65: // int + int
66: wcout << L"4 + 42 = ";
67: send(*t1, make_tuple(4, 42));
68: wcout << receive(*t1) << endl;
69:
70: // wstring + wstring
71: wcout << L"\"Hello, \" + \"World!\" = ";
72: send(*t2, make_tuple(wstring(L"Hello, "), wstring(L"World!")));
73: wcout << L'\"' << receive(*t2) << L'\"' << endl;
74:
75: // complex<double> + complex<double>
76: wcout << L"(3.0,4.0) + (2.0,-1.0) = ";
77: send(*t3, make_tuple(complex<double>(3.0, 4.0), complex<double>(2.0, -1.0)));
78: wcout << receive(*t3) << endl;
79:
80: // A + B
81: wcout << L"A(50) + B(25) = ";
82: send(*t4, make_tuple(A(50), B(25)));
83: wcout << receive(*t4) << endl;
84: }
This example produces the following output:
4 + 42 = 46
"Hello, " + "World!" = "Hello, World!"
(3.0,4.0) + (2.0,-1.0) = (5,3)
A(50) + B(25) = 75
Although the decltype keyword was created primarily for library developers, anyone who is interested in writing generic, reusable code can make use of this feature.
For more information about decltype, including how to use decltype and auto to declare a function that has a late-specified return type, see decltype Type Specifier and Stephan’s post Lambdas, auto, and static_assert: C++0x Features in VC10, Part 1.
Next time we’ll look at rvalue references, a Visual C++ language feature that can help you improve the performance of your applications that use Concurrency Runtime.
Comments
Anonymous
April 21, 2011
decltype(T1() + T2()), clean as it may look, is flawed, since it only works if T1 and T2 have public default constructors, which is a strong assumption. Instead, one might want to use a more universal, yet obscenely ugly piece of code: decltype((T1 )0 +(T2)0) it makes no assumptions about T1 and T2 except that they can be added with a '+'. Of course, some macro can be used to avoid rewriting this ugly code many times: #define OpType(T1, T2, Operator) decltype(*(T1 *)0 Operator *(T2 *)0)Anonymous
April 25, 2011
Hi, unfortunately it will be me again, who will be the "bad guy" but let me assure you that I mean/have no bad intentions, the only thing that I want is that for certain things to be done correctly. First question: Why do you write about features that has been already extensively explained by people who work for the same company you work for? If I want to read about auto: blogs.msdn.com/.../lambdas-auto-and-static-assert-c-0x-features-in-vc10-part-1.aspx, if I want to read about rvalues (by the way do us a favour and don't write about them for they are already comprehensively described in many places by many people): blogs.msdn.com/.../lambdas-auto-and-static-assert-c-0x-features-in-vc10-part-1.aspx. Also don't write about lambda nor static_assert (see first link I've provided). My advice for you: If you really feel that you have to write about something first check if subject you think of has been already explained (at least within your company resources), and if the answer is yes, then don't do it - what is the point of repeating something someone already did? But there are numbers of topics from C++11 that hasn't been "touched" by anyone (as far as I'm concerned) - so pick one and write about them. But my feeling is that all this will be ignored and you will write about rvalues anyway. And after that you'll write about lambdas. And after you someone else from MS will start it all over again until new features will be implemented in VS compiler and then they will be "discussed" repetitively by folks who instead of writing something original they write about things that already had been written tens of times if not more. RegardsAnonymous
April 26, 2011
MZ, thanks for sharing this. I showed the "clean" version just to demonstrate the concept. I'm glad that you pointed out the case where T1 or T2 doesn't have a default constructor. I don't think that you can apply it to this example, but you can also use so-called trailing return type syntax when you have a template function that takes parameters: template <typename T, typename U> auto add((T&& t, U&& u) -> decltype(forward<T>(t) + forward<U>(u)) { return forward<T>(t) + forward<U>(u); }Anonymous
April 26, 2011
Knowing, thanks for your feedback. I wish to apologize for this article not being helpful to you. I agree that there are a lot of great resources out there about the new language features in Visual C++. My intention was to illustrate how to use these features specifically with the PPL and Agents libraries. This article is actually 1 in a series of 5 (and yes, I already wrote about rvalue references and lambdas :D). I also linked to the pages that you cited, just to make sure that folks can read the whole story. It also sounds like there are areas of Visual C++ that you'd like to see covered in greater detail. We'd love to hear about what you're working on and what you'd like to read more about!Anonymous
April 27, 2011
The comment has been removed