Learn to Code via Tutorials on Repl.it!

← Back to all posts
C++ Variadic Lambdas For Beginners
xxpertHacker (390)

Preread

Please inform StudentFires or a confirmed associate of StudentFires, if there is anything that is vague, incorrect, hard to read, hard to understand, contains a misspelling, or is grammatically incorrect within this post.

If you know C++ to an advanced level, just jump the to the Final Code section by clicking the link. You won’t want to read this, since this post contains over 600 lines of markdown.

This post assumes elementary - high school understanding of mathematics and basic - moderate understanding of C++, or coding in general. It is expected that you know the following:

  • How to use the #include<> preprocessor directive
  • Comments (Single & multi-line)
  • Simple output
  • Variables
  • Loops
  • Data types
  • Basic arrays
  • Basic strings
  • Functions
  • Function overloading

The original writing for this post can be found here: @StudentFires/Tutorials.

This post was made as of a result of using memoization and attempting to Implement the idea in C++, in which I searched for the easiest and most sane way to use unpacked arguments, which when I showed off the idea and implementation, had a flaw that @Highwayman had a potential solution to, which, unfortunately failed.


C++ Variadic Lambdas For Beginners

Made with slight assistance by @DynamicSquid

I know not everyone will agree with this, and an even easier interface may exist, but let us continue regardless.

Sometimes, you want to use the same function name with different output depending on the number of arguments passed.
The first thing that might come to mind is function overloading.

Lets say we defined a function to add everything given to it together. This is an unrealistic example of course, but in an actual, real use case, your function could be extremely complicated.

Let’s start with the basics, using simply function overloading:

int sum(int num1)
{
    return num1;
}

int sum(int num1, int num2)
{
    return num1 + num2;
}

int sum(int num1, int num2, int num3)
{
    return num1 + num2 + num3;
}

This is already overkill; fortunately, there’s an easier way: default values.


Default Values

Default values can be used in a function by putting an equal sign immediately after the variable name; the value will be used if nothing is supplied as an argument.

An example:

int f(int x = 1);

“1” is used if nothing is supplied as an argument for our variable x.

Just a tip: if you wanted to do something like this:

int f(int x, int y = 1, int z); // note that “y” has a default value, but “z” doesn’t

Your code wouldn’t compile; Repl’s Clang++ compiler mandates that, if any parameters have default values, then all parameters past that parameter *must have default parameters.

If you wanted to declare a function over the main function without initializing it or writing the parameter names, you would write the data type, then just skip the name.

int f(int x, int y = 1, int z = 0)
{...}
// becomes
int f(int, int = 1, int = 0);

But under “main”, you may not put any default parameters whatsoever, even the same ones you used earlier.


In our sum” example, if nothing is supplied for the second argument, we could think of this as adding a zero, as this will not have an effect on the output in our example. Note, that in a real function, setting default parameters might not be as easy as putting “0”.

Thanks to the use of default parameters, this can be shortened to a single function.

int sum(int num1, int num2 = 0, int num3 = 0)
{
    return num1 + num2 + num3;
}

If we supplied it with just 1, it’s output would be 1, if we use 1, 0, it would still work just as well.

But what is we wanted to continue further?

A function can have a maximum of 256 parameters, this may or may not be implementation dependent, as I do not have a source; this was determined from secondhand experience.

We can’t made 256 functions! But back to the default parameters, would it be efficient to manually write out 256 variable name? No, it wouldn’t, obviously, especially if it did anything more complicated than basic addition, which you will need to do for any non-trivial program.

This is where variadic functions come into play. Variadic means having a variable arity. The arity of a function is the number of parameters it has. “Unary” means having an arity of zero, “binary” refers to an arity of two, while ternary means an arity of three.

Some examples of arity:

Unary operators: bit-wise “not” ~, logical “not” !, unary plus operator +, dereference operator *, bracket access operator [], unary negative operator -, address operator &, and dot operator .. Note: In C++, using the unary + is practically useless.

Binary operators: addition operator +, multiplication operator *, and the almighty comma operator ,.

Ternary operators: conditional operator ?:. That’s it, C++ only has a single operator that takes three arguments.


Most common operators in any coding language are binary, along with most common classical mathematical operations (Ex: ×, ÷, ^, √), although exceptions exist (Ex: ∫, |x|, ∑ I’ve encountered non-ternary summations before). You might have noticed I left out the plus and minus symbols. they’re binary operators right? Actually, they are great examples of variadic operators, it is assumed if you use +, -, or ± without anything preceding it, the first argument is treated as a zero. How would you evaluate (+7)? You would (should) look at this operation as if a zero preceded the + or - sign, thus the expression can be expanded to (0 + 7). (±7) would be (0 ± 7), which would evaluate to (+7) and (-7). But how do you look at (7-)?


Back to variadic functions: the last parameter of a function can be preceded by an ellipsis, this makes it a “rest” parameter. You can simply think of this “rest” parameter as the parameter that gets the rest of the parameters. This variable is no longer a variable, but instead now known as a parameter pack. You should be able to have packs of packs, don’t ask how or why.

To expand the parameter, you use the ellipsis, although in a slightly different way, usually (but not always) coming after the parameter pack name instead.

In C++, you can’t just do something like this:

int sum(int ...nums)
{
    return nums...;
}

There are multiple problems with this section of code;
first, it doesn’t actually add the arguments, but just returns them, second, this won’t compile, as this isn’t the proper format at all.

Variadic functions are awesome. Their capabilities are great, but they require creating templates.

Templates can appear very menacing to a new C++ programmer, they can also get quite complicated, and they can take up a lot of space, although, as with anything else though, this depends on your specific use for the template.

Check out @DynamicSquid’s post on Standard Template Library C++, to learn about templates, although variadic functions aren’t mentioned.

Unpacking and actually putting the arguments to use also gets quite complicated too.

Tip: To determine the number of parameters in a parameter pack, we can use sizeof...(x), where “x” is the parameter pack.

Here’s an example that DynamicSquid made, which I have since modified to be simpler and to make more sense:

template<typename ValueType>
int sum(ValueType valueType)
{
    std::cout << "first sum() function called\n";

    return valueType;
}

template<typename Current, typename ...Next>
int sum(Current currentValue, Next ...valueN)
{
    std::cout << "second sum() function called\n";

    int result = currentValue;

    //When there is only one parameter left, then the first sum() function will get called
    //This is known as overloading a function
	return result + sum(valueN...);
}

int main()
{
    // you can have as many values as you want!
    // sum(answer, 12, 89, 4, 45, 62, 25);
    // sum(6);
    // These would also work as well;
	// But “sum()” will throw an error

	cout << "the sum of (10 + 20 + 30) is " << sum(10, 20, 30);
}

Output:

second sum() function called
second sum() function called
first sum() function called
the sum of (10 + 20 + 30) is 60

That is ridiculously over complicated just to add some numbers; tell me it isn’t.

This is where my method comes into play. Other programming languages, such as JavaScript and Python, also make use of variadic functions, but their rest parameter is treated as an array. This is arguably easier to use.

#include <array>
#using namespace::std;

template<typename ...Args>
int sum(Args ...args)
{
	//the sizeof...() is length of the array, but the compiler can determine the size and type of the array for us
    array myArray = {args...};

    int total = 0;

    for(int i = 0; i < sizeof...(args); ++i)
        total += myArray[i];

	return total;
}

Notice I used array instead of int myArray[], this is because in C++, I reccommend using the standardized C++ version of anything that C has.

In this particular case, it has an advantage: we don't need to explicitly tell it what type it is.

T x[i] == array<T, i> x;
// or
T x[] == array x;

We can explicitly set the size of the array, but it must always be equal to or larger than the number of parameters.

array<sizeof...(valueN) + 1> myArray = {valueN...}; //  “+ 1” leaves one extra index in the array empty, which is set to “0”, like a normal array

Otherwise, this can be used to set a limit on the number of arguments that the function can be called with, this will cause a compile time error if called with too many arguments.

array<int, 5> Array = {valueN...}; //  “<..., 5>” Limits the rest parameter to, at most “5”

Templates or not, every argument must be of the same type. There are exceptions: for example, if you were to use {1, 2, 4.3}, it would upgrade to the largest data type used that can be mixed, so because we used {int, int, double}, it would use the “double.”

There are shorter ways that use C++’s parameter unpacking properly, which I highly recommend learning. For example, here’s the proper format for a simple sum, I still don’t fully understand how to properly use unpacking, but here it is anyways:

template<typename ...Args>
int sum(Args ...args)
{
	return (... + args); // Puts a “+” in between every argument
}

What if there was an easier way?

This is, after all, why you came here, right? Well, I have a solution.

This is especially helpful if you came from another language like Python or JavaScript, and are used to using arrays instead.

First of all, before I even start, I have to explain two of C++’s key words: auto, and decltype.


Type Deduction

Auto

C++ is still a staticially typed language; that won’t change anytime soon, but... C++ does support something that makes it feel dynamic.

When initializing Variables, it’s incredibly obvious what type the variable is supposed to be, right?

int x = 5;

Who would’ve guessed that our x was supposed to be an int? It has an int on the right side of it!

In many situations, initialization can be rewritten using the auto type; here’s an example:

auto x = 5;

Now, as I had said earlier, this doesn’t mean that it’s no longer statically typed, it is now set to the type of the right hand side, in our case that was a 5. You may not reassign it with something like, say, a string literal.

auto x = 5;
x = "5"; // Error
x = 9.6; // Okay, truncated to “9”

This can be used on anything that has a type.

auto x = "string";
// char x[] = "string";

It can even be used on functions!

int f(){...};

auto x = f;

...

x(); // == f()

auto y = main; // Perfectly okay, not sure why you’d need it

The compiler cannot determine the type, if the variable isn’t initialized, so something like this (below) wouldn’t work.

auto x; // Error

Decltype

decltype takes a single argument, and returns the type of the argument, whether it be a function return type, a complex expression, or what ever else can be used as a normal value.

It doesn’t evaluate the expression, so if you tried to call a function in the decltype(), the function wouldn’t actually be called.

auto x = 4; // What type is x? 4 is an int

decltype(x) y; // Since x is an int, y is an int too.

There is one more way to use decltype, that is by using decltype(auto). decltype(auto) and auto are similar.

One of the differences between auto and decltype(auto), is that other type qualifiers cannot be mixed with decltype(auto), whereas they can be used with auto.

Type qualifiers are keywords like const, words that aren’t types, but have an effect on the value or variable.

decltype(auto) tries to fully capture the type of the value.

const int x = 5; // const auto

decltype(auto) a = x; // const int
   const auto  b = x; // const int
         auto  c = x; // int

const decltype(auto) d = x; // Error, cannot mix “const” and “decltype”

If using shorthand variable initialization / declaration. you may not mix types.

auto x = 123, y = "123", z = (int[]){1, 2, 3}; // Error

Actually, as a matter of fact, you may not use either, auto, or decltype, with an array unless the array is type casted to a known type.

int a[] = {1, 2, 3};

auto b[] = {1, 2, 3}; // Isn’t it obvious what it should be though?
//This is an error

decltype(auto) c[] = {1, 2, 3}; // Error

auto d = (int[]){1, 2, 3}; // Okay, it was properly type casted to an int array

decltype(auto) e = (int[]){1, 2, 3}; // Okay; same as “d” 

array f = {1, 2, 3}; // C++ for the win!

Lambdas

Now why did I introduce type deduction? Because not everything has a type that has a word that can be simply typed. You’re probably confused, here: let me introduce you to a lambda. “Lambda” isn’t a keyword in C++, yet.

You should already know what a function is. A lambda is very similar, but it has slightly different use cases. You can use a lambda wherever a function can be used, but just because you can, doesn’t mean you should.

These are the formats for functions and lambdas, side by side.

“T” is a data type

T functionName(T parameter1, T parameter2, ...T parameterN)
{
    ...
}

auto functionName = [](T parameter1, T parameter2, ...T parameterN)
{
    ...
};

A lambda isn’t a function, it’s a variable. Secretly it’s a functor. So make sure to put a semi-colon after the body.

Each lambda is unique, you may not simply put a comma after the body and write another lambda, like you would a basic data type, like an int.

Many parts of a lambda are optional.

A lambda doesn’t need a a return type, but it may have one.

It must be in the format of a trailing return type. This is written in between the parameters and body, with an arrow facing right, with the arrow facing the return type, otherwise its return type is deduced, unless the compiler cannot deduce its return type, in which case you’ll get an error.

auto functionName = [](T parameter1, T parameter2, ...T parameterN) -> T
{
    ...
};

If the lambda doesn’t have any parameters, then it doesn’t need a set of parentheses.

T functionName()
{
    ...
}

auto functionName = []
{
    ...
};
// Unless it has a return type, in which case it’s necessary
auto functionName = []() -> T
{
    ...
};

The brackets are known as the “capture clause.” The capture clause changes what variables are allowed to be used inside the lambda.

See this example:

int x = 116;

auto increment1() // A function may return auto
{
    ++x; // Okay, it’s a normal function
}

auto increment2 = [] // void return type is assumed
{
    ++x; // Error
};

auto increment2 = [x] // void return type is assumed
{
    ++x; // Okay, x is inside the capture clause
};

Unlike a normal function, a lambda’s parameter may be of type auto.

auto f(auto x) // Error, cannot have “auto” as a function parameter
{
    return x;
}

auto f = [](auto x) // Okay
{
    return x; // Effectively returns any data type now
};

Knowing what’s been mentioned so far, the initial “sum” function from the beginning can be rewritten as a lambda.

int sum(int num1, int num2 = 0, int num3 = 0)
{
    return num1 + num2 + num3;
}

// Into

auto sum = [](int num1, int num2 = 0, int num3 = 0) -> int
{
    return num1 + num2 + num3;
};

As the parameters can be auto type, lambdas can be utilized, instead of functions. By using auto, a template is now unnecessary to make the rest parameter.

template<typename ...Args>
int sum(Args ...args)
{
	return (... + args);
}

Can be shortened to simply:

auto sum = [](auto ...args)
{
	return (... + args);
};
template<typename T>
struct Pair
{
	T value;

	Pair(T inValue)
	{
		value = inValue;
	}
};

//<int> specifies that you are giving an int as an argument
Pair<int> getValue(10); // Isn’t this supposed to be Pair.value(10)?

cout << "The value is " << pairOfValues.value << '\n'; // Did I kick you too early D.S.?

All arguments within the rest parameter must be of the same type for this to work.

Final Code

#include <iostream>

auto lambda = [](auto ...args)
{
    std::cout << "There were " << sizeof...(args) << " items passed to the function.\n\n";

    array argsArray = {args...};

    for(int i = 0; i < sizeof...(args); ++i)
        std::cout << argsArray[i] << ' ';
};

Using (1, 2, 3, 4, 5), the expected output would be:

There were 5 items passed to the function.

1 2 3 4 5

So, how would we rewrite the “sum” function from the very beginning, now using lambdas?

We already wrote the incredibly over complicated template version, so here’s the lambda version:

#include <array>
auto sum = [](auto ...nums)
{
    array numArray = {nums...};

    int total = 0;

    for(int i = 0, end = numArray.size() - 1; i < end; ++i)
        total += numArray[i];

    return total;
};

Look at that: it’s amazingly small! If I had to chose, I’d pick this lambda over that template function any day! But the more important question is, “would you do the same, or do you prefer the template(s)?”

Here’s the same thing using a range based for loop:

auto sum = [](auto ...nums)
{
    int total = 0;
	array numArray = {nums...};

    for(int num : numArray)
        total += num;

    return total;
};

We can go even further, and disregard the type that the argument is by directly using the arguments, as opposed to the array:

auto sum = [](auto ...nums)
{
    auto total = 0;

    for(auto num : {nums...})
        total += num;

    return total;
};

By using that last code fence and a variable to hold the number of arguments, you cannot mix types, unlike unsafe functions like the C printf function.

If you found this tutorial helpful, give it an up-vote, and use more variadic functions!


Theories and Fails

This section is currently under construction

#include <array>

lambda Ex using std::array<>

Explain why std::array needs both or no template parameters.

std::array<int> This isn’t okay

std::array okay, fail in our situation

Vectors are arrays that are unlike arrays in that they can be expanded at run time, whereas the length of an array is constant from the second the array is declared, thus unchangeable.

std::vector<int> This is okay

std::vector this fails

#include <array>

ex using decltype std::array
#include <vector>

Sources:


Totally Unrelated

Can you check out my bio and upvote the language request?