In programming, a function is a block of code that performs a computational task. (In practice, people write functions that perform many tasks, which is not very good, but it’s a topic beyond the purpose of this article). Functions are a fundamental concept of programming languages and C++ makes no exception. In fact, in C++ there is a large variety of functions that has evolved over time. In this article, I will give a brief walkthrough of this evolution starting with C++11. Since there are many things to talk about, I will not get into too many details on these topics but will provide various links for you to follow if you want to learn more.
Let’s start briefly with what he had before “modern” times.
Pre-C++11
Functions were available since the beginning of C++, whose first variant was called C with classes. This is how a function looks:
int add(int a, int b) { return a + b; }
This is what we call a non-member function or a free function, because it does not belong to any class. There are also member functions, that are part of a class/struct. These are also referred as methods (like in most other object-oriented programming languages), although this term is not used anywhere in the C++ standard. Here is an example:
class math { public: int add(int a, int b) { return a + b; } };
There are multiple kinds of functions, including the following:
- overloaded functions
int add(int a, int b) {return a + b;} double add(double a, double b) {return a + b;}
- static functions
static int add(int a, int b) {return a + b;} struct math { static int add(int a, int b) {return a + b;} }
- inline functions
inline int add(int a, int b) {return a + b;} struct math { inline int add(int a, int b); } int match::add(int a, int b) {return a + b;}
- operators
std::string operator+(std::string const & txt, int n) { return txt + std::to_string(n); // channels your JavaScript energy }
- constant member functions
class wrapper { public: wrapper(int a): value_(a) {} int get() const {return value_;} private: int value_; };
- virtual member functions
struct A { virtual void f() { std::cout << "A::f()\n"; } }; struct B : public A { virtual void f() { std::cout << "B::f()\n"; } };
- special class functions (default constructor, copy-constructor, copy-assignment operator, and destructor)
class wrapper { public: wrapper() : value_(0) {} wrapper(wrapper const & other) {value_ = other.value_; } wrapper& operator=(wrapper const & other) {if(this != &other) {value_ = other.value_;} } ~wrapper() {} private: int value_; };
All these are very simple examples but the point here is not to detail all these features that existed before modern C++. One thing that is missing here, though, is templates. Templates are blueprints that define families of functions or classes. The compiler instantiates actual overloads (in the case of function templates) from their use. Here is an example:
template <typename T> T add(T a, T b) { return a + b; }
Now that we’ve briefly looked at these, let’s see what changes modern C++ brought.
C++11
Variadic function templates
These are function templates with a variable number of arguments.
template <typename T> T add(T a, T b) { return a + b; } template <typename T, typename ...Ts> // [1] T add(T t, Ts ... rest) // [2] { return t + add(rest...); // [3] }
The ellipsis (...
) defines a parameter pack. We can have:
- a template parameter pack, such as
typename ... Ts
at line [1] - a function parameter pack, such as
Ts ... rest
at line [2] - a pack expansion, such as
add(rest...)
at line [3]
See also:
- Variadic templates in C++
- Variadic templates and the power of three dots
- Introduction to C++ variadic templates
Alternative function syntax
The return type of a function can be placed at the end of the function declaration, after the ->
token:
auto add(int a, int b) -> int { return a + b; }
In C++11, this is not of much help for non-template functions, but it’s important for some function templates. Consider a version of add()
that takes arguments of different types:
template<typename T, typename U> ??? add(T const & a, U const & b) { return a + b; }
What should the return type be? With the alternative function syntax we can place the return at the end of the expression and specify it with a decltype
expression:
template<typename T, typename U> auto add(T const & a, U const & b) -> decltype(a + b) { return a + b; }
See also:
- Pros and Cons of Alternative Function Syntax in C++
- Improved Type Inference in C++11: auto, decltype, and the new function declaration syntax
- Trailing return type (C++11)
constexpr functions
These are functions that can be evaluated at compile-time. The result of evaluating such a function is a compile-time value that can be used anywhere compile-time values are required. To make a function constexpr you need to define it with the constexpr
keyword, such as in the following example:
template <typename T> constexpr T add(T a, T b) { return a + b; } int main() { int arr[add(1,2)] = {1,2,3}; // [1] int a, b; std::cin >> a >> b; std::cout << add(a, b) << '\n'; // [2] }
Just because a function is declared constexpr
, doesn’t mean it is evaluated at compile-time. In the above example:
- the first call to
add
is evaluated at compile-time (line [1]) because all its arguments are integer literals - the second call to
add
(at line [2]) is evaluated at runtime because its arguments are only know at runtime
See also:
Override and final specifiers for virtual functions
These new specifies help us better describe virtual functions in derived classes.
The override
specifier used on a virtual function tells the compiler it is an overridden function of a base class virtual function. If the signature does not match, the compiler triggers an error.
struct A { virtual void f(int) {} virtual void g() {} }; struct B : public A { void f(int) override {} // OK void g(char) override {} // error, g() does not override anything };
The final
specifier tells a compiler a virtual function can longer be overridden in a derived class.
struct A { virtual void f() {} }; struct B : public A { void f() override final {} }; struct C : public B { void f() override {} // error, f cannot be overridden anymore };
It should be mentioned that the final
specifier can also be used on classes, in which case it prevents a class from being further derived.
See also:
- override specifier
- final specifier
- Virtual, final and override in C++
- override and final
- Modern C++: Safety and Expressiveness with override and final
More special member functions
Move semantics are not easy to describe in one sentence. Basically, it’s a language feature that enables the transfer of ownership of a resource from one object to another. Their purpose is improving performance by avoiding copies of resources that are not really necessary. For classes, these bring two new special functions: move constructor and move assignment operator:
struct buffer { buffer() // default constructor :data_(nullptr), size_(0) {} explicit buffer(size_t size) // constructor :data_(new char[size]), size_(size) {} ~buffer() // destructor { delete [] data_; } buffer(buffer const & other) // copy constructor : data_(new char[other.size_]) , size_(other.size_) { std::memcpy(data_, other.data_, size_); } buffer& operator=(buffer const & other) // copy assignment operator { if(this != &other) { delete [] data_; data_ = new char[other.size_]; size_ = other.size_; std::memcpy(data_, other.data_, size_); } return *this; } buffer(buffer&& other) // move constructor : data_(std::move(other.data_)) , size_(other.size_) { other.data_ = nullptr; other.size_ = 0; } buffer& operator=(buffer&& other) // move assignment operator { if(this != &other) { delete [] data_; data_ = std::move(other.data_); size_ = other.size_; other.data_ = nullptr; other.size_ = 0; } return *this; } private: char* data_; size_t size_; }; int main() { buffer b1; buffer b2(10); buffer b3 = b2; buffer b4 = std::move(b3); }
See also:
- What is move semantics?
- Understanding lvalues and rvalues in C and C++
- LESSON #5: MOVE SEMANTICS
- A little bit about std::move
Default and deleted functions
The special member functions (see above) can be generated by the compiler. However, this does not happen in some circumstances. For instance, if any user-defined constructor exists, a default constructor is not generated, or if a move constructor or move assignment operator is defined, then no copy constructor and copy assignment operator is generated. Rather than implementing these by yourself you can explicitly ask the compiler to generate the default implementation, using the = default
specifier.
struct foo { foo(int) {} // user-defined constructor foo() = default; // compiler generated default constructor };
On the other hand, sometimes we need some functions or some function overloads to not be available. We can prevent a function from being called by defining it with the = delete
specifier:
struct noncopyable { noncopyable() = default; noncopyable(noncopyable const &) = delete; noncopyable& operator=(noncopyable const &) = delete; };
Any function can be deleted, not just member functions, or special member functions (as shown in the previous example).
template <typename T> T add(T a, T b) { return a + b; } template <> int add<int>(int a, int b) = delete; int main() { add(1, 2); // error, this specialization is deleted }
See also:
- special member functions
- deleted functions
- Explicitly defaulted and deleted functions
- Tip of the Week #143: C++11 Deleted Functions (
= delete
) - C++11: defaulted and deleted functions
Lambdas
Lambdas are not really functions in C++ and the term lambda function is incorrect. The right term is lambda expressions. Lambdas are syntactic sugar for creating unnamed function objects (which can capture variables in scope). A function object is a class with an overloaded call operator.
int main() { auto add = [](int a, int b) { return a + b; }; add(1, 2); }
The compiler would generate something as follows (conceptually, as the details may vary):
int main() { class __lambda_1_10 { public: inline int operator()(int a, int b) const { return a + b; } }; __lambda_1_10 add = __lambda_1_10 {}; add.operator()(1, 2); }
Lambdas are useful for encapsulating a few lines of code that are then passed to functions such as general purpose algorithms or asynchronous functions.
int main() { std::vector<int> v {1, 5, 9, 2, 7}; std::sort(v.begin(), v.end(), [](int a, int b){return a > b;}); // sorts descending for(const auto & e : v) std::cout << e << '\n'; }
See also:
C++14
Function return type deduction
The alternative function syntax with trailing return type got simplified in C++14 with the compiler being able to deduce the return type from the return expression(s) present in the body of a function. Therefore, functions can be simplified as follows:
auto add(int a, int b) { return a + b; }
Again, this is more useful in template code:
template <typename T, typename U> auto add(T a, U b) { return a + b; }
See also:
- Automatic Return Type (C++11/14/20)
- Type deduction for functions
- C++14: Return Type Deduction for Normal Functions
Generic lambdas
A generic lambda is a lambda expression with at least one parameter specified with the auto
specifier.
int main() { using namespace std::string_literals; auto add = [](auto a, auto b) {return a + b;}; add(1, 2); add(1.0, 2.0); add("1"s, "2"s); }
This has the effect that the anonymous structure generated by the compiler has a template function call operator. For the above example, it would look, at least conceptually, as follows:
int main() { using namespace std::string_literals; class __lambda_8_16 { public: template <typename T0, typename T1> inline auto operator()(T0 a, T1 b) const { return a + b; } template<> inline int operator()(int a, int b) const { return a + b; } template<> inline double operator()(double a, double b) const { return a + b; } template<> inline std::string operator()(std::string a, std::string b) const { return std::operator+(a, b); } }; __lambda_8_16 add = __lambda_8_16{}; add.operator()(1, 2); add.operator()(1.0, 2.0); add.operator()(std::operator""s("1", 1UL), std::operator""s("2", 1UL)); }
See also:
C++20
Immediate functions
Constexpr functions from C++11 can be evaluated either at compile-time (if all arguments are compile-time values) or runtime. C++20 adds a new categories of functions, called immediate functions, that must be evaluated at compile-time. They always produce a compile-time expression and they are always visible only at compile-time. Symbols are not emitted for these functions, you cannot take the address of such functions, and tools such as debuggers will not be able to show them.
These functions are defined using the new consteval
keyword. Here is an example:
consteval int add(int const a, int const b) { return a + b; } int main() { constexpr int s1 = add(1, 2); // OK, compile-time evaluation int a = 12, b = 66; const int s2 = add(a, b); // error using fptr = int(int, int); fptr* padd = add; // error }
A consteval
specifier implies inline
. A function that is consteval
is a constexpr
function, and must satisfy the requirements applicable to constexpr
functions (or constexpr
constructors).
See also:
Abbreviated function templates
If you find template syntax ugly or difficult this feature is for you. It allows you to write function templates without using template syntax. Instead, you use the auto specifier to define function parameters. A function with a least one parameter specified with the auto specifier is an abbreviated function template:
auto add(auto a, auto b) { return a + b; }
The compiler transforms this into a function template:
template <typename T, typename U> auto add(T a, U b) { return a + b; }
These are actually called unconstrained abbreviated function templates because there are no constraints on the template arguments. However, you can specify constraints with the help of concepts. Such functions are called constrained abbreviated function templates.
auto add(std::integral auto a, std::integral auto b) { return a + b; }
This is the same as follows:
template <std::integral T, std::integral U> auto add(T a, U b) { return a + b; }
See also:
Lambda templates
The generic lambdas in C++14 have some shortcomings. For instance, consider this lambda:
auto add = [](auto a, auto b) {return a + b;};
The compiler generates the following function object:
struct _lambda_1 { template <typename T0, typename T1> inline auto operator()(T0 a, T1 b) const { return a + b; } };
But what if the intention is that the two arguments, a
and b
, to be of the same type? There is no way to model that in C++14. For this reason, C++20 introduces lambda template, that allows us to define generic lambdas using template syntax:
auto add = []<typename T>(T a, T b) {return a + b;};
See also:
- More Powerful Lambdas with C++20
- The Evolutions of Lambdas in C++14, C++17 and C++20
- Lambdas: From C++11 to C++20, Part 1
- Lambdas: From C++11 to C++20, Part 2
constexpr virtuals
You heard it right: in C++20, virtual functions can be defined as constexpr:
struct magic { constexpr virtual int def() const { return 0; } }; struct programming_magic : public magic { constexpr int def() const override { return 42; } }; constexpr int initval(magic const & m) { return m.def() + 1; } int main() { constexpr programming_magic pm; int arr[initval(pm)] = {0}; }
This doesn’t seem to have too many use-cases. I don’t see where we can use this too much, but it’s now possible.
See also:
Coroutines
This one is one of the major features of the C++20 standard. A coroutine is a function that has the ability to be suspended and resumed. Unfortunately, C++20 only defines a framework for the execution of coroutines, but does not define any coroutine types satisfying such requirements. That means, we need to either write our own or rely on 3rd party libraries for this. Such a library is the cppcoro library.
In C++20, there are three new keywords, for coroutines: co_await
, co_return
, and co_yield
. A function becomes a coroutine if it uses one of these three:
- the
co_await
operator to suspend execution until resumed - the
co_return
keyword to complete execution and optionally return a value - the
co_yield
keyword to suspend execution and return a value
Here is an example of a producer-consumer scenario (a coroutine produces new values and another coroutine consumes them as they become available):
#include <cppcoro/generator.hpp> cppcoro::generator<std::string> produce_items() { while (true) { auto v = rand(); using namespace std::string_literals; auto i = "item "s + std::to_string(v); print_time(); std::cout << "produced " << i << '\n'; co_yield i; } }
#include <cppcoro/task.hpp> cppcoro::task<> consume_items(int const n) { int i = 1; for(auto const& s : produce_items()) { print_time(); std::cout << "consumed " << s << '\n'; if (++i > n) break; } co_return; }
See also:
- coroutines
- A C++20 coroutine example
- My tutorial and take on C++20 coroutines
- Implementing Simple Futures with Coroutines
That’s about it for the time being. If I missed anything important, please let me know.
Great job! The material is well organized. In short, the base is C-style free function. Next is syntax sugar.
Dear Marius Bancila,
I hope this message finds you well. I wanted to take a moment to express my appreciation for your exceptional blog on C++. It has been an invaluable resource in my learning journey, providing me with substantial assistance. Your explanations and examples are clear, concise, and greatly enhance my understanding of the language.
While reading through your blog, I noticed a minor grammatical error in one particular paragraph. It seems to have caused a slight deviation in the intended meaning, leading to some ambiguity. Please allow me to point it out for your consideration:
[The final specifier tells a compiler a virtual function can (no/not?) longer be overridden in a derived class.]
I understand that even the most skilled writers can inadvertently overlook such mistakes, and it certainly does not diminish the overall quality of your blog. I merely wanted to bring it to your attention, as I believe it could enhance the clarity of the message you are conveying.
Once again, I want to emphasize how fantastic your blog has been for my C++ learning experience. The effort and dedication you have put into creating such a valuable resource are truly commendable. Thank you for sharing your knowledge and expertise with aspiring programmers like myself.
Best regards,
Tsunho Choy