Imagine a power to convert everything to a string: integers, vectors, booleans - all transformed into a message to output wherever we want. Seems handy and not too hard, so let’s dive into it – this was my first thought when writing a debug variable watcher for my pet project.

Although the expectation of making something easy may result in a few sleep-deprived nights, I encourage the reader to embark on this journey through basic template programming concepts.

Expected prerequisites are:

  • A compiler with decent support for C++20: MSVC 2022, clang 14, or gcc 11 would be good, as we aim for a future-oriented experience, not dwelling on legacy code.
  • A basic familiarity with C++ template syntax: we’ve all written something like template <typename T> T min(T a, T b);, haven’t we?1

Throughout the journey, we’ll progress from a trivial attempt to a satisfying solution, highlighting pitfalls and techniques along the way. So don’t be alarmed if the code doesn’t look great in the early iterations – it’s all part of the plan.

There would be a References section with all the links from an article. I’d treat it as a recommended reading.

The problem

Having a variable of type T, make a function makeString(T t) that will produce a string representation of t. As we could not know in advance how to convert an object of an arbitrary type into a string, consider making it easily extensible for any user-defined class.

Regarding extensibility, I’d like makeString to convert an object to a string by using the object’s std::string to_string() const member function if it’s present. It’s not always possible, so fall back to overloading or specialization if needed.

Something to start with

Setup a project

We need an empty project to start with. It’s at least 2023 outside, so I use CMake. It’s okay to create a console application project using your favorite IDE instead, but in case you ever plan to write a cross-platform code during the long and happy C++ career path, CMake is definitely worth trying.

cmake_minimum_required(VERSION 3.12)
project(makeString VERSION 0.1.0)
add_executable(makeString main.cpp)

set(CMAKE_CXX_EXTENSIONS OFF)      # no vendor-specific extensions
set_property(TARGET makeString PROPERTY CXX_STANDARD 20)

# Ask a compiler to be more demanding
if (MSVC)
    target_compile_options(makeString PUBLIC /W4 /WX /Za /permissive-)
else()
    target_compile_options(makeString PUBLIC -Wall -Wextra -pedantic -Werror)
endif()

A bit of test code to understand the goal

Let’s add a couple of use cases: assume we have structs A and B. A is a tag with no data, while B contains an integer. The only reason to use structs instead of classes here is to save some screen space on the public: visibility specifiers.

#include <iostream>
#include "makeString.hpp"

struct A
{
    std::string to_string() const { return "A"; }
};

struct B
{
    int m_i = 0;
    std::string to_string() const { return "B{" + std::to_string(m_i) + "}"; }
};

int main()
{
    A a;
    B b = {1};

    std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
              << std::endl;
}

A trivial template in makeString.hpp

And finally, a trivial template.

#pragma once
#include <string>

template <typename Object>
std::string makeString(const Object& object)
{
    return object.to_string();
}
Now we can build it, run it, and enjoy the output: a: A; b: B{1}.

This code has some problems, but let’s pretend we don’t see any until they arise during practical usage in the following sections of the article.

Function template specialization

But what if we’d like to convert an int to a string?

int main()
{
    A a;
    B b = {1};

    std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
              << "; pi: " << makeString(3)
              << std::endl;
}
Of course, the code above will result in a compilation error because there is no to_string() method for integers. Fortunately, we could provide a template specialization specifying that makeString<int> should have a special implementation. Let’s try:
// rather an attempt, than a solution
template <> std::string makeString(int i)
{
    return std::to_string(i);
}

Correct? Nope. The compiler does not find a matching template because the only part of the template signature allowed to specialize is the Object parameter:

template <typename Object> std::string makeString(const Object& object)

It’s possible to substitute an Object with int, but we can’t drop the const and reference qualifiers. Thus, the correct approach could be:

// template specialization: Object = int
template <> std::string makeString(const int& i)
{
    return std::to_string(i);
}

Well, although the function above works, it violates the “pass cheaply-copied types by value” guideline. Passing by-value is not only cheaper but also easier to optimize. Check out Arthur O’Dwyer’s blog for additional insight on the example of string_view.

Template function overloading

Both template and non-template functions participate in overloading. Function template reference has a lot of insight, Overload resolution of function template calls article has a couple of quick examples, but a summary is enough for now: both template and non-template functions are considered during an overload resolution to find the best match possible.

// not a template. This will be the best match for makeString(3),
// unless we explicitly specify that we want a template: makeString<int>(3)
std::string makeString(int i)
{
    return std::to_string(i);
}

Now we can drop the const int& specialization, compile, run, and enjoy overloaded makeString(int) and the approximate of pi: a: A; b: B{1}; pi: 3

For those who need precision, we could add more overloads:

// main.cpp
// ...
    std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
              << "; pi: " << makeString(3) << "; pi(double): " << makeString(3.1415926)
              << std::endl;
// makeString.hpp
//...
std::string makeString(float f)       { return std::to_string(f); }
std::string makeString(double d)      { return std::to_string(d); }
std::string makeString(long double d) { return std::to_string(d); }
// ... 5 more specializations ...

Selecting a single template from multiple implementations

The problem

In the middle of writing the copy-paste above, a reader might come across the idea of creating a single template instead. However, a problem arises when the compiler is unsure which template to instantiate for a call like makeString(3), leading to a compilation failure.

template <typename Object>
std::string makeString(const Object& object)
{
    return object.to_string();
}

template <typename Numeric>
std::string makeString(Numeric value)
{
    return std::to_string(value);
}

/*
main.cpp:21:40: error: call of overloaded ‘makeString(int)’ is ambiguous
 |               << "; pi: " << makeString(3)
 |                              ~~~~~~~~~~^~~
  note: candidate: std::string makeString(const Object&) [with Object = int;]
  note: candidate: std::string makeString(Numeric) [with Numeric = int;]
*/

During the overload resolution, the compiler examines only the function declarations, requiring the developer to provide a means of distinguishing overloads using function declarations only.

As a general rule, I’d consider restricting all template functions to prevent the compiler from instantiating them for incompatible types. Following this rule prevents extensibility issues when adding new template function overloads.

Spoiler: we could have used the code above almost as it is if we had read the Concepts section in advance
// concepts HasToString, IsNumeric, and IsString should be defined above

template <HasToString Object>
std::string makeString(Object&& object)
{
    return std::forward<Object>(object).to_string();
}

std::string makeString(IsNumeric auto value)
{
    return std::to_string(value);
}

template <IsString String>
std::string makeString(String&& s)
{
    return std::string(std::forward<String>(s));
}

For a deeper grasp of templates, we’ll proceed with SFINAE: a more verbose solution that was prevalent before the concepts appeared in C++20. However, readers who are eager to move ahead may skip this section and proceed directly to the perfect forwarding chapter and then to concepts.

Substitution Failure Is Not An Error: a harder, pre-C++20 approach

During overload resolution, the compiler will ignore the declaration if impossible to substitute the template parameter into it. That concept is known as Substitution Failure Is Not An Error.

Using wit and a little imagination, come up with a solution:

  • Incorporate an object.to_string() member function call into the declaration of std::string makeString(const Object& object) to prevent substitution of [Object = int].
  • Ensure that the declaration of std::string makeString(Numeric value) involves std::to_string(value) to prevent substitution in case of no such std::to_string overload before the compiler fails to compile the function body.

There are a few possible solutions:

1. Depend on the result of the to_string() member function call in the type parameter.

// will match only types with to_string() member function
template <typename Object,
          typename DummyType = decltype(std::declval<Object>().to_string())>
std::string makeString(const Object& object)
{
    return object.to_string();
}

The code above uses a DummyType that is the same as the type of Object::to_string() member function. The substitution will fail if there is no Object::to_string() thus excluding the template from the overloading resolution.

2. Trailing return type: access the function parameter name during the substitution

One could wonder, why don’t we use parameter variable names to enforce SFINAE? That’s because syntactically we’re enforcing it before the function parameters definition. Using trailing return type we could defer the SFINAE to the point where the function parameters are declared.

That’s my favorite way to use SFINAE using C++ 17, prior to the concepts introduction.

Let’s take a look:

// makeString.hpp
#pragma once
#include <string>

template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
    return object.to_string();
}

template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
    return std::to_string(value);
}

Here, an auto return type forces the compiler to look at the trailing return type, so it will detect the type of expression on the right of ->, for example, decltype(object.to_string()). If the substitution fails, the function is omitted from the overloads candidate list. That’s simple.

There is a std::enable_if template that could solve the same problem, but in my opinion, it’s more verbose. Therefore, I’d reserve its usage until it’s truly necessary.

Once again, compile, run, and enjoy: a: A; b: B{1}; pi: 3; pi(double): 3.141593. We’ve spent some time on it, it seems working and I think, it’s a good start.

Collections support in makeString()

Let’s level up makeString() with support for generic containers and amp up the coding fun. First, alter out test code with some cases:

// main.cpp
#include <vector>
#include <set>
// ...

    const std::vector<int> xs = {1, 2, 3};
    const std::set<float> ys = {4, 5, 6};
    const double zs[] = {7, 8, 9};

    std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
              << "; pi: " << makeString(3.1415926) << std::endl
              << "xs: " << makeString(xs) << "; ys: " << makeString(ys)
              << "; zs: " << makeString(zs)
              << std::endl;
Of course, the code is missing the necessary template. I like diving right into coding without much thought, but in this case, let’s be clever. A collection could be a vector, a set, or a C-array, but in a generic case, it is something iterable. We could use std::begin on something iterable. Thus we need a template that will accept something compatible with std::begin and iterate on it.

Now, start typing:

// makeString.hpp
// ...
template <typename Iterable>
auto makeString(const Iterable& iterable)
    -> decltype(makeString(*std::begin(iterable))) // (1)
{
    std::string result;
    for (const auto& i : iterable)
    {
        if (!result.empty())
            result += ';';
        result += makeString(i);
    }

    return result;
}
Just in case, a quick note (1): that’s a template that can accept something that could be passed to std::begin, and return the same type as the makeString for the first element of that collection.

Compile, run, so far so good:

a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000

However, that code has a pitfall we’re about to discover in the next section.

String parameter support in makeString()

Just in case, why don’t? It might streamline the makeString() usage in other templates. Let’s start by adding some test cases:

// main.cpp
// ...

    std::cout << makeString("Hello, ")
              << makeString(std::string_view("world"))
              << makeString(std::string("!!1"))
              << std::endl;
Compile, run, and brace yourself for a surprise: it works! But not quite as intended…
a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
72;101;108;108;111;44;32;0119;111;114;108;10033;33;49

Well, strings are iterable containers of chars. We also learned from the Integer Promotions chapter on the cppreference that the compiler promotes chars to integers when char overloading is absent. To address the issue, exclude the template for containers from matching string types.

Also, it could be reasonable to mark the makeString(char) as the deleted function to secure the pitfall forever:

std::string makeString(char c) = delete;
[build] makeString.hpp:62:6: note: candidate template ignored: substitution failure
          [with Iterable = const char (&)[8]]: call to deleted function 'makeString'
[build] auto makeString(Iterable&& iterable) -> decltype(makeString(*std::begin(iterable)))
[build]      ^                                           ~~~~~~~~~~

That’s a good time to remember about the type traits and std::enable_if.

Type traits and enable_if: a way to specialize template on a trait or a condition

A solution is to restrict the container’s makeString so it will fail substitution on strings, and write a new one. Consider that “string” is std::string, std::string_view, char*, const char*. Let’s express this using C++:

namespace traits
{
// generic type is not a string...
template <typename AnyType> inline constexpr bool isString = false;
// ... but these types are strings
template <> inline constexpr bool isString<std::string>      = true;
template <> inline constexpr bool isString<std::string_view> = true;
template <> inline constexpr bool isString<char*>            = true;
template <> inline constexpr bool isString<const char*>      = true;
}
But wait, what’s about const string, const string_view, const char* const? And there is a volatile keyword as well. Fortunately, we can avoid a lot of copy-paste by using a kind of types mapping: T -> T, const T -> T. For example, we could have written something like the code below:
namespace traits
{
namespace impl  // a "private" implementation
{
    // generic type is not a string...
    template <typename AnyType>
    inline constexpr bool isString = false;
    // ... but these types are strings
    template <> inline constexpr bool isString<std::string>      = true;
    template <> inline constexpr bool isString<std::string_view> = true;
    template <> inline constexpr bool isString<char*>            = true;
    template <> inline constexpr bool isString<const char*>      = true;
}

namespace Wheel
{
    template <typename T> struct remove_cv                   { using type = T; };
    template <typename T> struct remove_cv<const T>          { using type = T; };
    template <typename T> struct remove_cv<volatile T>       { using type = T; };
    template <typename T> struct remove_cv<const volatile T> { using type = T; };

    // shortcut to remove_cv<T>::type, saves a bit of stamina on fingers
    template <typename T> using remove_cv_t = typename remove_cv<T>::type;

    // enable_if only defined as a specialization enable_if<true, T>
    // thus failing substitution on any code that uses enable_if<false, T>;
    template <bool Condition, typename T> struct enable_if;
    template <typename T> struct enable_if<true, T> { using type = T; };

    // another shortcut
    template <bool B, class T> using enable_if_t = typename enable_if<B,T>::type;
}

// traits::isString<T> = traits::impl::isString<T>, but with remove_cv_t on T:
template <typename T>
inline constexpr bool isString = impl::isString<
                                    Wheel::remove_cv_t<T>
                                >;
}
However, we’ve already developed a habit of looking into a cppreference.com in advance, so we’ll use std::remove_cv_t and std::enable_if_t instead of reinventing the wheel:
// makeString.hpp
#pragma once
#include <string>
#include <type_traits>

namespace traits
{
namespace impl  // a "private" implementation
{
    // generic type is not a string...
    template <typename AnyType>
    inline constexpr bool isString = false;
    // ... but these types are strings
    template <> inline constexpr bool isString<std::string>      = true;
    template <> inline constexpr bool isString<std::string_view> = true;
    template <> inline constexpr bool isString<char *>           = true;
    template <> inline constexpr bool isString<const char *>     = true;
}

// isString<T> = impl::isString<T>, but with dropped const/volatile on T:
template <typename T>
inline constexpr bool isString = impl::isString<std::remove_cv_t<T>>;
}

template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
    return object.to_string();
}

template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
    return std::to_string(value);
}

// will fail substitution if `traits::isString<Iterable>`
// or when can't evaluate the type of makeString(*begin(container))
template <typename Iterable>
auto makeString(const Iterable& iterable)
    -> std::enable_if_t< !traits::isString<Iterable>,
                          decltype(makeString(*std::begin(iterable))) >
{
    std::string result;
    for (const auto& i : iterable)
    {
        if (!result.empty())
            result += ';';
        result += makeString(i);
    }

    return result;
}

// will fail substituition if `!traits::isString<String>`
template <typename String>
auto makeString(const String& s)
    -> std::enable_if_t< traits::isString<String>,
                         std::string >
{
    return std::string(s);
}
Now compile, run, swear:
a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
72;101;108;108;111;44;32;0world!!1

Getting an insight into what the template expands to

According to the output, "Hello, " were treated as a container. It could be easier to understand what’s happening using some tricks. There are at least a couple of ways: upload a minimal compilable example to a compiler analyzing tool or produce an intentional failure with a descriptive error message.

C++ Insights

Once the code compiles successfully, we could upload it to the cppinsights.io. There’s the link on a slightly stripped code.

And here we go, a part of the produced output:

namespace traits
{
  namespace impl
  {
    // ...
    template<>
    inline constexpr const bool isString<char *> = true;
    template<>
    inline constexpr const bool isString<const char *> = true;
    template<>
    inline constexpr const bool isString<char[8]> = false;
    template<>
    inline constexpr const bool isString<char> = false;

  }
  // ...
}

// ...

/* First instantiated from: insights.cpp:59 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
std::basic_string<char> makeString<char[8]>(const char (&iterable)[8])
{
    // ...
}
#endif
It’s quite obvious from the output above that makeString("Hello, ") is actually a makeString<char[8]>(/*reference-to-char[8]*/) and we have no such overload for impl::isString, so generic one takes place: impl::isString<char[8]> = false;.

Intentional compilation failure

It could happen, however, that code should not be submitted elsewhere or it does not compile. In this case, an intentional compilation failure could make insight into the deduced types and values. A deleted function will provide a good error context when called, while an undefined struct will provide an insight when instantiated:

// ...
template <typename... Args> bool fail_function(Args&&... args) = delete;
template <bool Value> struct fail_struct;    // never defined
// ...
static bool test1 = fail_function("Hello, ");                            // (1)
static fail_struct< traits::isString<decltype("Hello, ")> > test2 = {};  // (2)

And here is the compilation output:

error: use of deleted function bool fail_function(Args&& ...)
       [with Args = {const char (&)[8]}]
   40 | static bool test1 = fail_function("Hello, ");                            // (1)
      |                     ~~~~~~~~~~~~~^~~~~~~~~~~

error: variable fail_struct<false> test2 has initializer but incomplete type
   41 | static fail_struct< traits::isString<decltype("Hello, ")> > test2 = {};  // (2)
      |                                                             ^~~~~
Let’s extract the insight from the error message:

  • in fail_function("Hello, "), the argument type is a reference to a const char[8]. Despite it’s eligible for implicit conversion to the const char* it is different from the const char*.
  • in the fail_struct< traits::isString<...> > variable, argument is false, so the result of traits::isString<...> is false.

Finally, a makeString that accepts a string and works as expected

Just in case someone wants a solution to the isString<char[N]> trait It's that simple:
namespace impl
{
    // ...
    template <size_t N>
    inline constexpr bool isString<char[N]> = true;
    // ...
}

In previous chapters, we’ve invented some wheels and understood the idea behind type traits. Now it’s time to re-think it. Given the standard library, we could handle strings better. For example, let’s consider a type to be a string if a std::string could be explicitly constructed from it. That’s it.

namespace traits
{
template <typename T>
inline constexpr bool isString = std::is_constructible_v<std::string, T>;
}

Bring the pieces together, compile, and run.

// main.cpp
#include <iostream>
#include <vector>
#include <set>
#include "makeString.hpp"

struct A
{
    std::string to_string() const { return "A"; }
};

struct B
{
    int m_i = 0;
    std::string to_string() const { return "B{" + std::to_string(m_i) + "}"; }
};

int main()
{
    A a;
    B b = {1};

    const std::vector<int> xs = {1, 2, 3};
    const std::set<float> ys = {4, 5, 6};
    const double zs[] = {7, 8, 9};

    std::cout << "a: " << makeString(a) << "; b: " << makeString(b)
              << "; pi: " << makeString(3.1415926) << std::endl
              << "xs: " << makeString(xs) << "; ys: " << makeString(ys)
              << "; zs: " << makeString(zs)
              << std::endl;

    std::cout << makeString("Hello, ")
              << makeString(std::string_view("world"))
              << makeString(std::string("!!1"))
              << std::endl;
}

// makeString.hpp
#pragma once
#include <string>
#include <type_traits>

namespace traits
{
template <typename T>
inline constexpr bool isString = std::is_constructible_v<std::string, T>;
}

template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
    return object.to_string();
}

template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
    return std::to_string(value);
}

// will fail substitution if `traits::isString<Iterable>`
// or when can't evaluate the type of makeString(*begin(container))
template <typename Iterable>
auto makeString(const Iterable& iterable)
    -> std::enable_if_t< !traits::isString<Iterable>,
                          decltype(makeString(*std::begin(iterable))) >
{
    std::string result;
    for (const auto& i : iterable)
    {
        if (!result.empty())
            result += ';';
        result += makeString(i);
    }

    return result;
}

// will fail substituition if `!traits::isString<String>`
template <typename String>
auto makeString(const String& s)
    -> std::enable_if_t< traits::isString<String>,
                         std::string >
{
    return std::string(s);
}
a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
Hello, world!!1

Now criticize: it’s still a good start! At least, the problem is that the std::string is also constructed from the parameter, even if there is a temporary that could be perfectly forwarded to a constructor. That brings us to the next chapter.

Perfect forwarding: avoid unnecessary copying and allow the use of non-copyable types

In the code below, a temporary is made by getSomeString() and bound to an lvalue-reference parameter. The referenced string is then copied to a return value. Thanks to a copy elision, no further copies were made, but in the upshot, one unnecessary copy occurs.

template <typename String>
auto makeString(const String& s)
    -> std::enable_if_t< traits::isString<String>,
                         std::string >
{
    return std::string(s);
}

// ...

std::string getSomeString();
std::cout << makeString(getSomeString());

In addition to suboptimal performance, the previous approach does not allow non-copyable types, such as std::unique_ptr. However, by leveraging rvalue or forwarding references, we can omit unnecessary copy and avoid the requirement of copyable type. The std::string constructor can then efficiently steal its content without unnecessary copying.

While the only option for a non-template parameter is an rvalue overload, template parameters can utilize universal (or forwarding) references. In essence, a forwarding reference accepts the same reference type (lvalue, rvalue, const, or non-const) as passed by the caller. A forwarding reference syntax is TemplateType&& parameter.

I strongly encourage looking at the great article above for additional details.

Just in case, a quick summary and we’re going to fix our templates.

Parameter TypeTemplateNon-Template
const &const lvalue referenceconst lvalue reference
&lvalue referencelvalue reference
&&the same as has been passed by the caller,
may be: (const-) lvalue or rvalue
rvalue reference

This way, for every pass-by-reference in makeString.hpp we’ll pass the parameter by a universal reference:

// ...
template <typename Object>
auto makeString(Object&& object) -> decltype(object.to_string())
//...
template <typename Iterable>
auto makeString(Iterable&& iterable)
// ...
template <typename String>
auto makeString(String&& s)
// ...
Is that it? Not yet. A function parameter is always an lvalue inside the function. We have to provide an rvalue if possible, to allow std::string to steal the temporary’s content.

Thoroughful developer may consider all the possible cases:

  • An lvalue or (const-)lvalue reference passed to makeString(): pass further as an appropriate lvalue reference because we should not invalidate an lvalue by moving from it;
  • A temporary or another type of rvalue passed to makeString(): pass it via rvalue-reference to allow moving from it.

It might sound complicated, but in essence std::forward casts a function parameter to whatever it was at the function invocation. So, a fast developer uses std::forward for universal reference whenever a possible move is intended.

A forwarding pitfall: a temporary may be ‘consumed’ and invalidated by the callee

However, a cautious developer would warn us that a move may occur wherever std::forward is used. So never use a variable after forwarding:

#include <string>
#include <cassert>
#include <vector>

template <typename Arg>
void foo(Arg&& a)
{
    std::vector<std::string> v;
    for(int i = 0; i < 2; i++)
        v.push_back(std::forward<Arg>(a)); // surprise on the 2nd iteration

    assert(v.front() == v.back());         // fail
}

int main()
{
    auto getMessage = []() -> std::string { return "why does it fail?"; };
    foo(getMessage());
}
The example above may seem synthetic, but it does occur in the wild. Consider the following situation:

  1. There is a callback that receives a parameter using perfect forwarding: logCallback(std::forward<Event>(logEvent));. Looks good and conventional.
  2. Later, a developer forgot to remove std::forward while implementing multiple callbacks support: for (auto& sink : logSinks) sink.logCallback(std::forward<Event>(event));. This oversight creates a ticking time bomb.
  3. The issue goes unnoticed during basic testing with only a single callback or an lvalue reference.
  4. Undefined behavior occurs in rare cases where multiple callbacks involve rvalue reference, leading to unexpected behavior.

Thus a developer should have a strong intuition to remove std::forward once a possibility of multiple recipients of the forwarded value occurs.

Temporary containers

A tricky case is handling an rvalue container as a parameter. Although the container may be temporary, its elements are constructed in a usual way (for example, it’s possible to get an address of the element) so they are treated like lvalues. Thus, a forcible std::move should be used to steal elements’ content from a temporary container:

// will fail substitution if `traits::isString<Iterable>`
// or when can't evaluate the type of makeString(*begin(container))
template <typename Iterable>
auto makeString(Iterable&& iterable)
    -> std::enable_if_t< !traits::isString<Iterable>,
                          decltype(makeString(*std::begin(iterable)))
                        >
{
    std::string result;
    for (auto&& i : iterable)
    {
        if (!result.empty())
            result += ';';

        // a constexpr if, so the compiler can omit unused branch and
        // allow non-copyable types usage
        if constexpr (std::is_rvalue_reference_v<decltype(iterable)>)
            result += makeString(std::move(i));
        else
            result += makeString(i);
    }
    return result;
}

‘if constexpr’ in this context enables selecting a strategy in the compile-time based on the container’s value category. Unlike the regular if statement, only one branch of constexpr if/else is compiled at template instantiation, allowing for the use of a type that is either movable or copyable.

The MS STL implementation of std::vector::resize utilizes this approach to choose between copy and move strategies. When resizing, it moves existing items to a new buffer if the value type has a ’noexcept’ move constructor, and copies them otherwise2.

Also, there are alternative approaches. For instance, introducing a separate template to encapsulate the copy-or-move decision into two overloads is demonstrated in the std::move_if_noexcept example on cppreference.

Bringing all the pieces together, let’s look at the perfect forwarding makeString below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// makeString.hpp
#pragma once
#include <string>
#include <type_traits>

namespace traits
{
template <typename T>
inline constexpr bool isString = std::is_constructible_v<std::string, T>;
}

template <typename Object>
auto makeString(Object&& object)
    -> decltype(std::forward<Object>(object).to_string())
{
    return std::forward<Object>(object).to_string(); // (see a note below)
}

template <typename Numeric>
auto makeString(Numeric value) -> decltype(std::to_string(value))
{
    return std::to_string(value);
}

// will fail substituition if `!traits::isString<String>`
template <typename String>
auto makeString(String&& s)
    -> std::enable_if_t< traits::isString<String>,
                         std::string >
{
    return std::string(std::forward<String>(s));
}

// will fail substitution if `traits::isString<Iterable>`
// or when can't evaluate the type of makeString(*begin(container))
template <typename Iterable>
auto makeString(Iterable&& iterable)
    -> std::enable_if_t< !traits::isString<Iterable>,
                          decltype(makeString(*std::begin(iterable)))
                        >
{
    std::string result;
    for (auto&& i : iterable)
    {
        if (!result.empty())
            result += ';';

        // a constexpr if, so the compiler can omit unused branch and
        // allow non-copyable types usage
        if constexpr (std::is_rvalue_reference_v<decltype(iterable)>)
            result += makeString(std::move(i));
        else
            result += makeString(i);
    }
    return result;
}
And a couple of new tests:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <vector>
#include <set>
#include "makeString.hpp"

struct A
{
    std::string to_string() const { return "A"; }
};

struct B
{
    int m_i = 0;
    std::string to_string() const { return "B{" + std::to_string(m_i) + "}"; }
};

struct NonCopyable
{
    std::string m_s;
    NonCopyable(const char* s) : m_s(s)  {}
    NonCopyable(NonCopyable&&) = default;
    NonCopyable(const NonCopyable&) = delete;

    std::string   to_string() const &  { return m_s; }
    std::string&& to_string() &&       { return std::move(m_s); }
};

int main()
{
    A a;
    B b = {1};

    const std::vector<int> xs = {1, 2, 3};
    const std::set<float> ys = {4, 5, 6};
    const double zs[] = {7, 8, 9};

    std::cout << "a: " << makeString(a) << "; b: " << makeString(b) 
              << "; pi: " << makeString(3.1415926) << std::endl
              << "xs: " << makeString(xs) << "; ys: " << makeString(ys) 
              << "; zs: " << makeString(zs)
              << std::endl;

    std::cout << makeString("Hello, ") 
              << makeString(std::string_view("world")) 
              << makeString(std::string("!!1")) 
              << std::endl;

    auto makeVector = []()
    { 
        std::vector<NonCopyable> v;
        v.emplace_back("two ");
        v.emplace_back(" non-copyables");
        return v; 
    };

    std::cout << makeString(makeVector())
              << std::endl;
}
One might wonder, why did I forward an object here when calling to_string() on it. And what’s the strange syntax on NonCopyable::to_string.
template <typename Object>
auto makeString(Object&& object)
    -> decltype(std::forward<Object>(object).to_string())
{
    return std::forward<Object>(object).to_string(); // (see a note below)
}
// ...
struct NonCopyable
{
    // ...
    std::string to_string() const &  { return m_s; }
    std::string to_string() &&       { return std::move(m_s); }
};

The std::forward here forwards the object to its object.to_string() member function, and corersponding NonCopyable::to_string methods feature the overloading on *this value category. This way we can allow an rvalue-overload of NonCopyable::to_string to steal the content of temporary avoiding unnecessary copying.

Overloading member functions on reference qualifiers

A ref-quilified member function allows compiler to choose a specific overloading based on refevence type of *this. A simple example from the corresponding chapter of the cppreference:

#include <iostream>

struct S
{
    void f() &  { std::cout << "lvalue\n"; }
    void f() && { std::cout << "rvalue\n"; }
};

int main()
{
    S s;
    s.f();            // prints "lvalue"
    std::move(s).f(); // prints "rvalue"
    S().f();          // prints "rvalue"
}
Regarding the NonCopyable::to_string(), in case of calling it on a temporary, it’s safe to assume that the temporary will be obsolete after the call, so we can steal its content by moving the NonCopyable::m_s into a return value. Of course, we still don’t need && at the return type, because the object returned by value is rvalue itself.

Perhaps, that was the only time since 2011 that I’ve seen justified use of return std::move, because nowadays we may rely on guaranteed copy elision that usually works better than std::move for the return value3.

There is an article on another interesting use case of ref-qualified member functions on this blog: Practical usage of ref-qualified member function overloading

Concepts: a modern approach to set requirements on a template type

Now it’s time for a pivot: with C++20, we can avoid writing SFINAE code and embrace concepts.

A concept is a named set of requirements. There’s a detailed article on cppreference, but here’s the gist: it allows distinguishing between multiple template overloads based on the template parameter, similar to SFINAE. However, concepts offer enhanced clarity by separating the declaration from usage and enabling easier combinations, resulting in a more concise code.

A trivial trait-based concept and a requirement

Let’s make minimal modifications to our makeString(String&& s) and makeString(Numeric value) overloads and take a look at the first attempt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace traits
{
template <typename T> 
inline constexpr bool isString = std::is_constructible_v<std::string, T>;
}

// a concepts that uses a constexpr bool trait to check its applicability
template <typename T>
concept IsString = traits::isString<T>;

template <IsString String>
std::string makeString(String&& s) 
{
    return std::string(std::forward<String>(s));
}

// a concept uses requires clause: code in braces should compile
template <typename T>
concept HasStdConversion = requires (T number) { std::to_string(number); };

std::string makeString(HasStdConversion auto number)
{
    return std::to_string(number);
}

A few explanations are below:

  • Line #9: a trivial concept is created to demonstrate the usage of a constexpr bool as a requirement. Although the <concepts> provides std::constructible_from, we’ll use a custom trait-based concept as a showcase.
  • Line #11-12: IsString concept is used instead of the typename keyword to keep typing to a minimum. Please note that there is no more need for SFINAE or trailing return type.
  • In line #19, a requirement is introduced: HasStdConversion is a concept that requires the compilation of std::to_string(number); with T number to ensure that the std::to_string conversion exists. Still not rocket science.
  • Line #21: HasStdConversion is utilized to constrain the auto type of the parameter. This approach is equivalent to the one from lines #11-12 and requires even less tying. However, regarding the code clarity, my personal preference is to use the template keyword to clearly indicate that the following method is a template.

Concepts: conjunctions and disjunctions

Now the code can be further cleaned up using <concepts> instead of hand-written traits. Also, drop the traits namespace and rewrite every function declaration to use concepts instead of declarations. Here we go:

  • Assume that type T is a string, if std::string is std::constructible_from it:

    template <typename T>
    concept IsString = std::constructible_from<std::string, T>;
    
    template <IsString String>
    std::string makeString(String&& s) 
    {
        return std::string(std::forward<String>(s));
    }

  • HasStdConversion will require that std::to_string could be called on its argument:

    template <typename T>
    concept HasStdConversion = requires (T number) { std::to_string(number); };
    
    template <HasStdConversion Numeric>
    std::string makeString(Numeric number)
    {
        return std::to_string(number);
    }

  • HasToString is a concept that requires object.to_string() with Object object parameter to be valid and return something that is std::convertible_to<std::string>. In this case, a requirement will check the expression type using another concept, in addition to a base requirement to compile the code in the braces:

    template <typename T>
    concept HasToString = requires (T&& object) 
    { 
        {object.to_string()} -> std::convertible_to<std::string>; 
    };
    
    template <HasToString Object>
    std::string makeString(Object&& object) 
    {
        return std::forward<Object>(object).to_string();
    }

  • Finally, the tricky part. Similarly to the SFINAE approach, IsContainer is a requirement on type to be iterable: requires (T&& container) { std::begin(container); }. However, a string is a container, too, and this will lead to ambiguity when calling makeString("hello") because both overloads will match. There is a point in our code where requires clause is useful to require a boolean constraint.

    template <typename T>
    concept IsContainer = requires (T&& container) { std::begin(container); };
    
    template <typename Iterable>
        requires (IsContainer<Iterable> && !IsString<Iterable>)
    std::string makeString(Iterable&& iterable) 
    {
        //...
    }

I’d prefer, however, to define IsContainer just as something iterable, and IsString as a IsContainer that can be used to construct an std::string. This way when something is both IsString and IsContainer, the first one will have a priority because the more constrained concept is preferred by the compiler.

That are some great articles within-depht explanation of conjunctions, disjunctions and ordering by constraints that I highly recommend to look at.

Perhaps, it’s a good point to summarize the code and sync-up with a reader’s imagination by providing the full source:

// makeString.hpp
#pragma once
#include <string>
#include <type_traits>
#include <concepts>

template <typename T>
concept IsContainer = requires (T&& container) { std::begin(container); };

// IsString is more constrained than IsContainer, 
// so it will have a priority wherever possible
template <typename T>
concept IsString = IsContainer<T> && std::constructible_from<std::string, T>;

template <typename T>
concept HasStdConversion = requires (T number) { std::to_string(number); };

template <typename T>
concept HasToString = requires (T&& object) 
{ 
    {object.to_string()} -> std::convertible_to<std::string>; 
};


template <HasStdConversion Numeric>
std::string makeString(Numeric number)
{
    return std::to_string(number);
}

template <HasToString Object>
std::string makeString(Object&& object) 
{
    return std::forward<Object>(object).to_string();
}

template <IsString String>
std::string makeString(String&& s) 
{
    return std::string(std::forward<String>(s));
}

template <IsContainer Iterable>
std::string makeString(Iterable&& iterable) 
{
    std::string result;
    for (auto&& i : iterable)
    {
        if (!result.empty())
            result += ';';

        // a constexpr if, so the compiler can omit unused branch and
        // allow non-copyable types usage
        if constexpr (std::is_rvalue_reference_v<decltype(iterable)>)
            result += makeString(std::move(i));
        else 
            result += makeString(i);
    }
    return result;
}

Compile, run: it works!

Ordering overloads by constraints

Speaking of extensibility, let’s imagine that our makeString is a library header, and we’re unwilling to modify it. In such a scenario, consider the class that is both IsContainer and HasToString:

struct C
{
    std::string m_string;
  
    auto begin() const { return std::begin(m_string); }
    auto begin()       { return std::begin(m_string); }
    auto end() const   { return std::end(m_string); }
    auto end()         { return std::end(m_string); }

    std::string to_string() const { return "C{\"" + m_string + "\"}"; }
};

int main()
{
    // ...
    std::cout << makeString(makeVector())
              << std::endl
              << makeString( C { "a container with its own to_string()" } )
              << std::endl;

}
There is an ambiguity in the makeString(C{...}) call because the compiler cannot determine whether the IsContainer or HasToString overload is better to apply.
[build] main.cpp:58:18: error: call to 'makeString' is ambiguous
[build]               << makeString( C { "a container with its own to_string()" } )
[build]                  ^~~~~~~~~~
[build] makeString.hpp:37:13: note: candidate function [with Object = C]
[build] std::string makeString(Object&& object) 
[build]             ^
[build] makeString.hpp:49:13: note: candidate function [with Iterable = C]
[build] std::string makeString(Iterable&& iterable) 
[build]             ^

Those familiar with the era of SFINAE can imagine the magnitude of the tragedy, and those who have already seen Andrzej’s article on ordering by constraints can imagine the solution.

The ambiguity arises because two constrained methods have the same priority, with neither constraint subsuming the other. The solution is straightforward: introduce a third overload that is more restrictive than the conflicting ones.

struct C
{
    std::string m_string;
  
    auto begin() const { return std::begin(m_string); }
    auto begin()       { return std::begin(m_string); }
    auto end() const   { return std::end(m_string); }
    auto end()         { return std::end(m_string); }

    std::string to_string() const { return "C{\"" + m_string + "\"}"; }
};

template <typename Container>
    requires IsContainer<Container> && HasToString<Container>
std::string makeString(Container&& c)
{
    return std::forward<Container>(c).to_string();
}

int main()
{
    // ...
    std::cout << makeString(makeVector())
              << std::endl
              << makeString( C { "a container with its own to_string()" } )
              << std::endl;
}

This way, the ambiguity is resolved by introducing a new rule, preserving the library implementation.

Variadic templates

There is another topic the developer could greatly benefit from when using it carefully. Back in the days, we had functions with a varied arguments count. Still, there is a set of printf-like functions, where the developer feeds a variable amount of parameters, and the compiler tries to save him from numerous pitfalls.

For instance, Unreal Engine incorporates the format string checks into a custom preprocessor, while std::format or fmt provides us an error message that is a bit cryptic but much better than an Undefined Behavior in runtime.

Having that, why don’t provide a variadic template for makeString("xs: ", xs, "; and the double is: ", PI)? At least, that’s a nice exercise.

Basic syntax and considerations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename... Args>
constexpr int uselessCount(Args&&... args)
{
    return sizeof...(args);
}

int main(int argc, char**)
{
    return uselessCount(1,2,3) 
          + uselessCount();
}

Let me describe this masterpiece line-by-line:

  • Line #1 declares a parameter pack using an obvious syntax. A parameter pack is is a template parameter that accepts zero or more template arguments.
  • Line #2: ‘Args&&… args’ is a forwarding reference to a pack.
  • Line #4: a special sizeof... that is evaluated at a compile-time to a number of pack’s arguments.
  • Line #9: an obvious way to use.
  • Line #10 may be unobvious, but still correct: a pack of zero arguments is provided.

Parameter pack expansion and recursive functions

Function arguments may contain a regular parameter alongside a parameter pack. For recursive functions, it provides a convenient way to break the recursion once the parameter pack is empty. Consider the makeString(T&& first, Rest&&... rest) in pseudocode:

  • Rest has parameters: return makeString(first) + makeString(rest...);.
  • Rest is an empty parameter pack: return makeString(first);

While the pseudocode above demonstrates the idea, it does not apply to our code as-is: makeString(T&& first, Rest&&... rest) accepts one or more parameters. Since we already have a set of single-parameter implementations, an empty-pack call will be ambiguous. The solution is constraining the variadic overload to accept at least two parameters.

Here is an approach from the past: a recursive function makeString(First&& first, Second&& second, Rest&&... rest) accepts at least two parameters plus an optional variadic pack. It converts the first parameter to a string and calls makeString(second, rest...) recursively. Once the rest... pack is empty, it expands to a non-recursive makeString(second) call.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// makeString.hpp
// ... single-parameter implementations cut from the listing  ...

template <typename First, typename Second, typename... Rest>
std::string makeString(First&& first, Second&& second, Rest&&... rest)
{
    return makeString(std::forward<First>(first)) 
         + makeString(std::forward<Second>(second), std::forward<Rest>(rest)...); 
}

// main.cpp
// ... stripped ...
std::cout << makeString("a ", 
                        std::string_view("variadic " ), 
                        std::string("with a double: "), 
                        3.14)
          << std::endl;

Line #8 introduces a parameter pack expansion to the reader: compiler expands expression(pack)... to a comma-separated list of zero or more expression(argument)patterns. For example, std::forward<Rest>(rest)... expands to:

  • If Rest has no parameters: expands to nothing, ignoring the std::forward<Rest>(rest)... expression on line #8.
  • If Rest has one parameter: std::forward<Rest_0>(rest_0) is added, resulting in a call like makeString(std::forward<Second>(second), std::forward<Rest_0>(rest_0));.
  • If Rest has N parameters, each parameter is individually expanded using the enclosing expression: std::forward<Rest_0>(rest_0), std::forward<Rest_1>(rest_1), ..., std::forward<Rest_N>(rest_N).

In summary, amount of parameters to makeString call on line #8 depends on the size of the parameter pack. The whole std::forward<Rest_#>(rest_#) expression is “copy-pasted” for each added parameter. It’s important to note that this behavior applies to any kind of packed expression, not just std::forward, so f(g(pack)...) would be expanded as f(g(pack_0), g(pack_1), ..., g(pack_N)) accordingly.

Given that, compile, run, and enjoy!

a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
Hello, world!!1
two ; non-copyables
C{"a container with its own to_string()"}
a variadic with a double: 3.140000

Constrained version

However, we can improve further. Instead of using the Second&& second parameter to avoid overloading conflicts, we can leverage constraints by requiring sizeof...(Rest) > 0:

template <typename First, typename... Rest>
    requires (sizeof...(Rest) > 0)
std::string makeString(First&& first, Rest&&... rest)
{
    return makeString(std::forward<First>(first)) 
         + makeString(std::forward<Rest>(rest)...); 
}

Less code is less of a mental load on the reading developer, so it’s a bit better. Can it be even better?

Fold expressions

Well, since C++ 17 we have fold expressions. Having a parameter pack pack and an unary or binary operation ‘x’ we can expand (pack x ...) to (pack_0 x (pack_1 x pack_2)) and so on. There are several options including a left-associative fold (... pack x) with dots on the left, a right-associative with dots on the right, an option for unary or binary operations, an init variable for an empty pack, and so on.

To keep this article size reasonable, I dare to forward the reader to the Fluent C++ blog for in-depth details: part 1 - basics and part 2 - advanced usage.

As we’re dealing with strings, I expect the left-associative fold over operator+= to be more performant than using operator+ because the latter will produce temporaries for holding intermediate results in case of multiple strings involved: return ((a + b) + c) + d result in 3 temporaries besides a, b, c, while return ((a += b) += c) += d; avoids them.

template <typename... Pack>
    requires (sizeof...(Pack) > 1)
std::string makeString(Pack&&... pack)
{
    return (... += makeString(std::forward<Pack>(pack)));
}

Finally, I’d call it a day and summarize:

  • A template function can (and probably should) be constrained to let the compiler fail early and provide a concise error context in case of misuse. Even if it has no overloads yet.
  • SFINAE is a reasonable fallback when concepts are unavailable.
  • Use static_assert and intentional compilation failures to get an insight into template expansion in case of misunderstanding.
  • Perfect forwarding is our friend: it may offer better performance and relax the ‘copyable’ requirement on the arguments.
    • … but not for trivial types. Some times are cheaper to pass by-copy than by-reference.
  • Variadic templates are a convenient way to process multiple arguments in a row. Fold expression might be even better.

The code I agree with

Let’s sync up on the result of this journey:

// makeString.hpp
#pragma once
#include <string>
#include <type_traits>
#include <concepts>

template <typename T>
concept IsContainer = requires (T&& container) { std::begin(container); };

// IsString is more constrained than IsContainer, so it will have a priority wherever possible
template <typename T>
concept IsString = IsContainer<T> && std::constructible_from<std::string, T>;

template <typename T>
concept HasStdConversion = requires (T number) { std::to_string(number); };

template <typename T>
concept HasToString = requires (T&& object) 
{ 
    {object.to_string()} -> std::convertible_to<std::string>; 
};


template <HasStdConversion Numeric>
std::string makeString(Numeric number)
{
    return std::to_string(number);
}

template <HasToString Object>
std::string makeString(Object&& object) 
{
    return std::forward<Object>(object).to_string();
}

template <IsString String>
std::string makeString(String&& s) 
{
    return std::string(std::forward<String>(s));
}

template <IsContainer Iterable>
std::string makeString(Iterable&& iterable) 
{
    std::string result;
    for (auto&& i : iterable)
    {
        if (!result.empty())
            result += ';';

        // a constexpr if, so the compiler can omit unused branch and
        // allow non-copyable types usage
        if constexpr (std::is_rvalue_reference_v<decltype(iterable)>)
            result += makeString(std::move(i));
        else 
            result += makeString(i);
    }
    return result;
}

template <typename... Pack>
    requires (sizeof...(Pack) > 1)
std::string makeString(Pack&&... pack)
{
    return (... += makeString(std::forward<Pack>(pack)));
}
// main.cpp
#include <iostream>
#include <vector>
#include <set>
#include "makeString.hpp"

struct A
{
    std::string to_string() const { return "A"; }
};

struct B
{
    int m_i = 0;
    std::string to_string() const { return "B{" + std::to_string(m_i) + "}"; }
};

struct NonCopyable
{
    std::string m_s;
    NonCopyable(const char* s) : m_s(s)  {}
    NonCopyable(NonCopyable&&) = default;
    NonCopyable(const NonCopyable&) = delete;

    std::string   to_string() const &  { return m_s; }
    std::string&& to_string() &&       { return std::move(m_s); }
};

struct C
{
    std::string m_string;
  
    auto begin() const { return std::begin(m_string); }
    auto begin()       { return std::begin(m_string); }
    auto end() const   { return std::end(m_string); }
    auto end()         { return std::end(m_string); }

    std::string to_string() const { return "C{\"" + m_string + "\"}"; }
};

template <typename Container>
    requires IsContainer<Container> && HasToString<Container>
std::string makeString(Container&& c)
{
    return std::forward<Container>(c).to_string();
}

int main()
{
    A a;
    B b = {1};

    const std::vector<int> xs = {1, 2, 3};
    const std::set<float> ys = {4, 5, 6};
    const double zs[] = {7, 8, 9};

    std::cout << "a: " << makeString(a) << "; b: " << makeString(b) 
              << "; pi: " << makeString(3.1415926) << std::endl
              << "xs: " << makeString(xs) << "; ys: " << makeString(ys) 
              << "; zs: " << makeString(zs)
              << std::endl;

    std::cout << makeString("Hello, ") 
              << makeString(std::string_view("world")) 
              << makeString(std::string("!!1")) 
              << std::endl;

    auto makeVector = []()
    { 
        std::vector<NonCopyable> v;
        v.emplace_back("two ");
        v.emplace_back(" non-copyables");
        return v; 
    };

    std::cout << makeString(makeVector())
              << std::endl
              << makeString( C { "a container with its own to_string()" } )
              << std::endl;

    std::cout << makeString("a ", std::string_view("variadic "), std::string("with a double: "), 3.14)
              << std::endl;
}
And the output is:
a: A; b: B{1}; pi: 3.141593
xs: 1;2;3; ys: 4.000000;5.000000;6.000000; zs: 7.000000;8.000000;9.000000
Hello, world!!1
two ; non-copyables
C{"a container with its own to_string()"}
a variadic with a double: 3.140000

References

A section is also known as “I saw an interesting link somewhere in a text wall above”:


  1. Actually, some of use wrote template <class T> – that doesn’t matter, but I prefer a template <typename T> because the int is not a class name ↩︎

  2. std::vector features strong exception safety guarantee: its remains unchanged if resize() throws an exception. In order to maintain that it has to keep initial buffer as a backup if element’s type move constructor is not declated as noexcept(true) ↩︎

  3. a copy elision is better that moving a return value because the object will be constructed already in the scope of the caller without move construction. It also donsn’t take any references of the return value thus making the optimizer’s work easier. ↩︎