C++ Core Guidelines: Rules for Variadic Templates
Variadic templates are a typical feature of C++: from the user’s perspective, they are easy to use, but from the implementor’s perspective, they look pretty scary. Today’s post is mainly about the implementor’s perspective.
Before I write about the details of variadic temples, I want to mention my introduction to this post briefly. I often wear two heads when I teach C++: one for the user and one for the implementor. Features such as templates are easy to use but challenging to implement. This significant gap is typically for C++ and, I assume, more profound than in other mainstream programming languages such as Python, Java, or C. Honestly, I have no problem with this gap. I call this gap abstraction, and it is an essential part of the power of C++. The art of the implementer of the library or framework is to provide easy-to-use (complicated to misuse) and stable interfaces. If you don’t get the point, wait for the next section, when I develop std::make_unique.
Today’s post is based on three rules:
- T.100: Use variadic templates when you need a function that takes a variable number of arguments of a variety of types
- T.101: ??? How to pass arguments to a variadic template ???
- T.102: ??? How to process arguments to a variadic template ???
You can already guess it. The three rules are title-only; therefore, I make one story out of the first three.
As promised, I want to develop std::make_unique. std::make_unique is a function template that returns a dynamically allocated object protected by a std::unique_ptr. Let me show you a few use cases.
// makeUnique.cpp #include <memory> struct MyType{ MyType(int, double, bool){}; }; int main(){ int lvalue{2020}; std::unique_ptr<int> uniqZero = std::make_unique<int>(); // (1) auto uniqEleven = std::make_unique<int>(2011); // (2) auto uniqTwenty = std::make_unique<int>(lvalue); // (3) auto uniqType = std::make_unique<MyType>(lvalue, 3.14, true); // (4) }
Based on this use case, what are the requirements of std::make_unique?
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- It should deal with an arbitrary number of arguments. The std::make_unique calls get 0, 1, and 3 arguments.
- It should deal with lvalues and rvalues. The std::make_unique call in line (2) gets an rvalue and an lvalue in line (3). The last one even gets an rvalue and an lvalue.
- It should forward its arguments unchanged to the underlying constructor. This means the constructor of std::unique_ptr should get an lvalue/rvalue if std::make_unique gets an lvalue/rvalue.
These requirements are typically for factory functions such as std::make_unique, std::make_shared, std::make_tuple, and std::thread. Both rely on two powerful features of C++11:
- Variadic templates
- Perfect forwarding
Now, I want to create my factory function createT. Let me start with perfect forwarding.
Perfect Forwarding
First of all: What is perfect forwarding?
- Perfect forwarding allows you to preserve an argument’s value category (lvalue/rvalue) and
const
/volatile
modifiers.
Perfect forwarding follows a typical pattern consisting of a universal reference and std::forward.
template<typename T> // (1) void create(T&& t){ // (2) std::forward<T>(t); // (3) }
The three parts of the pattern to get perfect forwarding are:
- You need a template parameter T: typename T
- Bind T by universal reference, also known as perfect forwarding reference: T&& t
- Invoke std::forward on the argument: std::forward<T>(t)
The key observation is that T&& (line 2) can bind an lvalue or an rvalue and that std::forward (line 3) does the perfect forwarding.
It’s time to create the prototype of the createT factory function, which should behave at the end, such as makeUnique.cpp. I just replaced std::make_unique with the createT call, added the createT factory function, and commented the lines (1) and (4) out. Additionally, I removed the header <memory> (std::make_unique) and added the header <utility>(std::foward).
// createT1.cpp #include <utility> struct MyType{ MyType(int, double, bool){}; }; template <typename T, typename Arg> T createT(Arg&& arg){ return T(std::forward<Arg>(arg)); } int main(){ int lvalue{2020}; //std::unique_ptr<int> uniqZero = std::make_unique<int>(); // (1) auto uniqEleven = createT<int>(2011); // (2) auto uniqTwenty = createT<int>(lvalue); // (3) //auto uniqType = std::make_unique<MyType>(lvalue, 3.14, true); // (4) }
Fine. An rvalue (line 2) and an lvalue (line 3) pass my test.
Variadic Templates
Sometimes dots are essential. Putting exactly nine dots at the right place and line (1) and line (4) work.
// createT2.cpp #include <utility> struct MyType{ MyType(int, double, bool){}; }; template <typename T, typename ... Args> T createT(Args&& ... args){ return T(std::forward<Args>(args) ... ); } int main(){ int lvalue{2020}; int uniqZero = createT<int>(); // (1) auto uniqEleven = createT<int>(2011); // (2) auto uniqTwenty = createT<int>(lvalue); // (3) auto uniqType = createT<MyType>(lvalue, 3.14, true); // (4) }
How does the magic work? The three dots stand for an ellipse. By using them Args, or args become a parameter pack. To be more precise, Args is a template parameter pack, and args is a function parameter pack. You can only apply two operations to a parameter pack: you can pack or unpack it. If the ellipse is left of Args the parameter pack is packed; if the ellipse is right of Args the parameter pack is unpacked. In the case of the expression (std::forward<Args>(args)…) this means the expression is unpacked until the parameter pack is consumed, and a comma is just placed between the unpacked components. This was all.
CppInsight helps you to look under the curtain.
Now, I’m nearly done. Here is my createT factory function.
template <typename T, typename ... Args> T createT(Args&& ... args){ return T(std::forward<Args>(args) ... ); }
The two missing steps are.
- Create a std::unique_ptr<T> instead of a plain T
- Rename my function make_unique.
I’m done.
std::make_unique
template <typename T, typename ... Args> std::unique_ptr<T> make_unique(Args&& ... args){ return std::unique_ptr<T>(new T(std::forward<Args>(args) ... )); }
I forgot to scare you. Here is the scary part of my post.
printf
Of course, you know the C function printf. This is its signature: int printf( const char* format, … );. printf is a function that can get an arbitrary number of arguments. Its power is based on the macro va_arg and is, therefore, not typesafe.
Thanks to variadic templates, printf can be rewritten in a typesafe way.
// myPrintf.cpp #include <iostream> void myPrintf(const char* format){ // (3) std::cout << format; } template<typename T, typename ... Args> void myPrintf(const char* format, T value, Args ... args){ // (4) for ( ; *format != '\0'; format++ ) { // (5) if ( *format == '%' ) { // (6) std::cout << value; myPrintf(format + 1, args ... ); // (7) return; } std::cout << *format; // (8) } } int main(){ myPrintf("\n"); // (1) myPrintf("% world% %\n", "Hello", '!', 2011); // (2) myPrintf("\n"); }
How does the code work? If myPrintf is invoked with only a format string (line 1), line (3) is used. In the case of line (2), the function template (line 4) is applied. The function templates loops (line 5) as long as the format symbol does not equal `\0`. Two control flows are possible if the format symbol is not equal to `\0`. First, if the format starts with ‘%‘ (line 6), the first argument value is displayed, and myPrintf is once more invoked, but this time with a new format symbol and an argument less (line 7). Second, the format symbol is displayed if the format string does not start with ‘%’ (line 8). The function myPrintf (line 3) is the end condition for the recursive calls.
The output of the program is as expected.
What’s next?
One rule to variadic templates is left. Afterward, the guidelines continue with template metaprogramming. I’m unsure how deep I should dive into template metaprogramming in my next post.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!