theater 311730 1280

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.

 

theater 311730 1280

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:

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?

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    1. It should deal with an arbitrary number of arguments. The std::make_unique calls get 0, 1, and 3 arguments.
    2. 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.
    3. 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:

    1. Variadic templates
    2. 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:

    1. You need a template parameter T: typename T
    2. Bind T by universal reference, also known as perfect forwarding reference: T&& t
    3. 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.

    createT2 1

    createT2 2

    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.

    1. Create a std::unique_ptr<T> instead of a plain T
    2. 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.

     myPrintf

    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)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *