Chapter 6

Move Semantics and enable_if<>

One of the most prominent features C++11 introduced was move semantics. You can use it to optimize copying and assignments by moving (“stealing”) internal resources from a source object to a destination object instead of copying those contents. This can be done provided the source no longer needs its internal value or state (because it is about to be discarded).

Move semantics has a significant influence on the design of templates, and special rules were introduced to support move semantics in generic code. This chapter introduces these features.

6.1 Perfect Forwarding

Suppose you want to write generic code that forwards the basic property of passed arguments:

• Modifyable objects should be forwarded so that they still can be modified.

• Constant objects should be forwarded as read-only objects.

• Movable objects (objects we can “steal” from because they are about to expire) should be forwarded as movable objects.

To achieve this functionality without templates, we have to program all three cases. For example, to forward a call of f() to a corresponding function g():

basics/move1.cpp

#include <utility>
#include <iostream>

class X {
  
};

void g (X&) {
  std::cout << "g() for variable ";
}
void g (X const&) {
  std::cout << "g() for constant ";
}
void g (X&&) {
  std::cout << "g() for movable object ";
}

// let f() forward argument val to g():
void f (X& val) {
  g(val);             // val is non-const lvalue => calls g(X&)
}
void f (X const& val) {
  g(val);             // val is const lvalue => calls g(X const&)
}
void f (X&& val) {
  g(std::move(val));  // val is non-const lvalue => needs std::move() to call g(X&&)
}

int main()
{
  X v;              // create variable
  X const c;        // create constant

  f(v);             // f() for nonconstant object calls f(X&) => calls g(X&)
  f(c);             // f() for constant object calls f(X const&) => calls g(X const&)
  f(X());           // f() for temporary calls f(X&&) => calls g(X&&)
  f(std::move(v));  // f() for movable variable calls f(X&&) => calls g(X&&)
}

Here, we see three different implementations of f() forwarding its argument to g():

void f (X& val) {
  g(val);             // val is non-const lvalue => calls g(X&)
}
void f (X const& val) {
  g(val);             // val is const lvalue => calls g(X const&)
}
void f (X&& val) {
  g(std::move(val));  // val is non-const lvalue => needs std::move() to call g(X&&)
}

Note that the code for movable objects (via an rvalue reference) differs from the other code: It needs a std::move() because according to language rules, move semantics is not passed through.1 Although val in the third f() is declared as rvalue reference its value category when used as expression is a nonconstant lvalue (see Appendix B) and behaves as val in the first f(). Without the move(), g(X&) for nonconstant lvalues instead of g(&&) would be called.

If we want to combine all three cases in generic code, we have a problem:

template<typename T>
void f (T val) {
  g(T);
}

works for the first two cases, but not for the (third) case where movable objects are passed.

C++11 for this reason introduces special rules for perfect forwarding parameters. The idiomatic code pattern to achieve this is as follows:

template<typename T>
void f (T&& val) {
  g(std::forward<T>(val));  // perfect forward val to g()
}

Note that std::move() has no template parameter and “triggers” move semantics for the passed argument, while std::forward<>() “forwards” potential move semantic depending on a passed template argument.

Don’t assume that T&& for a template parameter T behaves as X&& for a specific type X. Different rules apply! However, syntactically they look identical:

X&& for a specific type X declares a parameter to be an rvalue reference. It can only be bound to a movable object (a prvalue, such as a temporary object, and an xvalue, such as an object passed with std::move(); see Appendix B for details). It is always mutable and you can always “steal” its value.2

T&& for a template parameter T declares a forwarding reference (also called universal reference).3 It can be bound to a mutable, immutable (i.e., const), or movable object. Inside the function definition, the parameter may be mutable, immutable, or refer to a value you can “steal” the internals from.

Note that T must really be the name of a template parameter. Depending on a template parameter is not sufficient. For a template parameter T, a declaration such as typename T::iterator&& is just an rvalue reference, not a forwarding reference.

So, the whole program to perfect forward arguments will look as follows:

basics/move2.cpp

#include <utility>
#include <iostream>

class X {
  
};

void g (X&) {
  std::cout << "g() for variable ";
}
void g (X const&) {
  std::cout << "g() for constant ";
}
void g (X&&) {
  std::cout << "g() for movable object ";
}

// let f() perfect forward argument val to g():
template<typename T>
void f (T&& val) {
  g(std::forward<T>(val));   // call the right g() for any passed argument val
}

int main()
{
  X v;              // create variable
  X const c;        // create constant

  f(v);             // f() for variable calls f(X&) => calls g(X&)
  f(c);             // f() for constant calls f(X const&) => calls g(X const&)
  f(X());           // f() for temporary calls f(X&&) => calls g(X&&)
  f(std::move(v));  // f() for move-enabled variable calls f(X&&) => calls g(X&&)
}

Of course, perfect forwarding can also be used with variadic templates (see Section 4.3 on page 60 for some examples). See Section 15.6.3 on page 280 for details of perfect forwarding.

6.2 Special Member Function Templates

Member function templates can also be used as special member functions, including as a constructor, which, however, might lead to surprising behavior.

Consider the following example:

basics/specialmemtmpl1.cpp

#include <utility>
#include <string>
#include <iostream>

class Person
{
  private:
    std::string name;
  public:
    // constructor for passed initial name:
    explicit Person(std::string const& n) : name(n) {
        std::cout << "copying string-CONSTR for ’" << name << "’ ";
    }
    explicit Person(std::string&& n) : name(std::move(n)) {
        std::cout << "moving string-CONSTR for ’" << name << "’ ";
    }
    // copy and move constructor:
    Person (Person const& p) : name(p.name) {
        std::cout << "COPY-CONSTR Person ’" << name << "’ ";
    }
    Person (Person&& p) : name(std::move(p.name)) {
        std::cout << "MOVE-CONSTR Person ’" << name << "’ ";
    }
};

int main()
{
  std::string s = "sname";
  Person p1(s);              // init with string object => calls copying string-CONSTR
  Person p2("tmp");          // init with string literal => calls moving string-CONSTR
  Person p3(p1);             // copy Person => calls COPY-CONSTR
  Person p4(std::move(p1));  // move Person => calls MOVE-CONST
}

Here, we have a class Person with a string member name for which we provide initializing constructors. To support move semantics, we overload the constructor taking a std::string:

• We provide a version for string object the caller still needs, for which name is initialized by a copy of the passed argument:

Person(std::string const& n) : name(n) {
    std::cout << "copying string-CONSTR for ’" << name << "’ ";
}

• We provide a version for movable string object, for which we call std::move() to “steal” the value from:

Person(std::string&& n) : name(std::move(n)) {
   std::cout << "moving string-CONSTR for ’" << name << "’ ";
}

As expected, the first is called for passed string objects that are in use (lvalues), while the latter is called for movable objects (rvalues):

std::string s = "sname";
Person p1(s);              // init with string object => calls copying string-CONSTR
Person p2("tmp");          // init with string literal => calls moving string-CONSTR

Besides these constructors, the example has specific implementations for the copy and move constructor to see when a Person as a whole is copied/moved:

Person p3(p1);             // copy Person => calls COPY-CONSTR
Person p4(std::move(p1));  // move Person => calls MOVE-CONSTR

Now let’s replace the two string constructors with one generic constructor perfect forwarding the passed argument to the member name:

basics/specialmemtmpl2.hpp

#include <utility>
#include <string>
#include <iostream>

class Person
{
  private:
    std::string name;
  public:
    // generic constructor for passed initial name:
    template<typename STR>
    explicit Person(STR&& n) : name(std::forward<STR>(n)) {
        std::cout << "TMPL-CONSTR for ’" << name << "’ ";
    }

    // copy and move constructor:
    Person (Person const& p) : name(p.name) {
        std::cout << "COPY-CONSTR Person ’" << name << "’ ";
    }
    Person (Person&& p) : name(std::move(p.name)) {
        std::cout << "MOVE-CONSTR Person ’" << name << "’ ";
    }
};

Construction with passed string works fine, as expected:

std::string s = "sname";
Person p1(s);              // init with string object => calls TMPL-CONSTR
Person p2("tmp");          //init with string literal => calls TMPL-CONSTR

Note how the construction of p2 does not create a temporary string in this case: The parameter STR is deduced to be of type char const[4]. Applying std::forward<STR> to the pointer parameter of the constructor has not much of an effect, and the name member is thus constructed from a null-terminated string.

But when we attempt to call the copy constructor, we get an error:

Person p3(p1);               // ERROR

while initializing a new Person by a movable object still works fine:

Person p4(std::move(p1));  // OK: move Person => calls MOVE-CONST

Note that also copying a constant Person works fine:

Person const p2c("ctmp");  //init constant object with string literal
Person p3c(p2c);           // OK: copy constant Person => calls COPY-CONSTR

The problem is that, according to the overload resolution rules of C++ (see Section 16.2.4 on page 333), for a nonconstant lvalue Person p the member template

template<typename STR>
Person(STR&& n)

is a better match than the (usually predefined) copy constructor:

Person (Person const& p)

STR is just substituted with Person&, while for the copy constructor a conversion to const is necessary.

You might think about solving this by also providing a nonconstant copy constructor:

Person (Person& p)

However, that is only a partial solution because for objects of a derived class, the member template is still a better match. What you really want is to disable the member template for the case that the passed argument is a Person or an expression that can be converted to a Person. This can be done by using std::enable_if<>, which is introduced in the next section.

6.3 Disable Templates with enable_if<>

Since C++11, the C++ standard library provides a helper template std::enable_if<> to ignore function templates under certain compile-time conditions.

For example, if a function template foo<>() is defined as follows:

template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
}

this definition of foo<>() is ignored if sizeof(T) > 4 yields false.4 If sizeof(T) > 4 yields true, the function template instance expands to

void foo() {
}

That is, std::enable_if<> is a type trait that evaluates a given compile-time expression passed as its (first) template argument and behaves as follows:

• If the expression yields true, its type member type yields a type:

– The type is void if no second template argument is passed.

– Otherwise, the type is the second template argument type.

• If the expression yields false, the member type is not defined. Due to a template feature called SFINAE (substitution failure is not an error), which is introduced later (see Section 8.4 on page 129), this has the effect that the function template with the enable_if expression is ignored.

As for all type traits yielding a type since C++14, there is a corresponding alias template std::enable_if_t<>, which allows you to skip typename and ::type (see Section 2.8 on page 40 for details). Thus, since C++14 you can write

template<typename T>
std::enable_if_t<(sizeof(T) > 4)>
foo() {
}

If a second argument is passed to enable_if<> or enable_if_t<>:

template<typename T>
std::enable_if_t<(sizeof(T) > 4), T>
foo() {
  return T();
}

the enable_if construct expands to this second argument if the expression yields true. So, if MyType is the concrete type passed or deduced as T, whose size is larger than 4, the effect is

MyType foo();

Note that having the enable_if expression in the middle of a declaration is pretty clumsy. For this reason, the common way to use std::enable_if<> is to use an additional function template argument with a default value:

template<typename T,
         typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}

which expands to

template<typename T,
         typename = void>
void foo() {
}

if sizeof(T) > 4.

If that is still too clumsy, and you want to make the requirement/constraint more explicit, you can define your own name for it using an alias template:5

template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;

template<typename T,
         typename = EnableIfSizeGreater4<T>>
void foo() {
}

See Section 20.3 on page 469 for a discussion of how std::enable_if is implemented.

6.4 Using enable_if<>

We can use enable_if<> to solve our problem with the constructor template introduced in Section 6.2 on page 95.

The problem we have to solve is to disable the declaration of the template constructor

template<typename STR>
Person(STR&& n);

if the passed argument STR has the right type (i.e., is a std::string or a type convertible to std::string).

For this, we use another standard type trait, std::is_convertible<FROM,TO>. With C++17, the corresponding declaration looks as follows:

template<typename STR,
         typename = std::enable_if_t<
                      std::is_convertible_v<STR, std::string>>>
Person(STR&& n);

If type STR is convertible to type std::string, the whole declaration expands to

template<typename STR,
         typename = void>
Person(STR&& n);

If type STR is not convertible to type std::string, the whole function template is ignored.6

Again, we can define our own name for the constraint by using an alias template:

template<typename T>
using EnableIfString = std::enable_if_t<
                         std::is_convertible_v<T, std::string>>;

template<typename STR, typename = EnableIfString<STR>>
Person(STR&& n);

Thus, the whole class Person should look as follows:

basics/specialmemtmpl3.hpp

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
using EnableIfString = std::enable_if_t<
                         std::is_convertible_v<T,std::string>>;

class Person
{
  private:
    std::string name;
  public:
    // generic constructor for passed initial name:
    template<typename STR, typename = EnableIfString<STR>>
    explicit Person(STR&& n)
     : name(std::forward<STR>(n)) {
        std::cout << "TMPL-CONSTR for ’" << name << "’ ";
    }
    // copy and move constructor:
    Person (Person const& p) : name(p.name) {
        std::cout << "COPY-CONSTR Person ’" << name << "’ ";
    }
    Person (Person&& p) : name(std::move(p.name)) {
        std::cout << "MOVE-CONSTR Person ’" << name << "’ ";
    }
};

Now, all calls behave as expected:

basics/specialmemtmpl3.cpp

#include "specialmemtmpl3.hpp"

int main()
{
  std::string s = "sname";
  Person p1(s);              // init with string object => calls TMPL-CONSTR
  Person p2("tmp");          // init with string literal => calls TMPL-CONSTR
  Person p3(p1);             // OK => calls COPY-CONSTR
  Person p4(std::move(p1));  // OK => calls MOVE-CONST
}

Note again that in C++14, we have to declare the alias template as follows, because the _v version is not defined for type traits that yield a value:

template<typename T>
using EnableIfString = std::enable_if_t<
                         std::is_convertible<T, std::string>::value>;

And in C++11, we have to declare the special member template as follows, because as written the _t version is not defined for type traits that yield a type:

template<typename T>
using EnableIfString
  = typename std::enable_if<std::is_convertible<T, std::string>::value
                           >::type;

But that’s all hidden now in the definition of EnableIfString<>.

Note also that there is an alternative to using std::is_convertible<> because it requires that the types are implicitly convertible. By using std::is_constructible<>, we also allow explicit conversions to be used for the initialization. However, the order of the arguments is the opposite is this case:

template<typename T>
using EnableIfString = std::enable_if_t<
                         std::is_constructible_v<std::string, T>>;

See Section D.3.2 on page 719 for details about std::is_constructible<> and Section D.3.3 on page 727 for details about std::is_convertible<>. See Section D.6 on page 734 for details and examples to apply enable_if<> on variadic templates.

Disabling Special Member Functions

Note that normally we can’t use enable_if<> to disable the predefined copy/move constructors and/or assignment operators. The reason is that member function templates never count as special member functions and are ignored when, for example, a copy constructor is needed. Thus, with this declaration:

class C {
  public:
    template<typename T>
    C (T const&) {
        std::cout << "tmpl copy constructor ";
    }
    …
};

the predefined copy constructor is still used, when a copy of a C is requested:

C x;
C y{x};  // still uses the predefined copy constructor (not the member template)

(There is really no way to use the member template because there is no way to specify or deduce its template parameter T.)

Deleting the predefined copy constructor is no solution, because then the trial to copy a C results in an error.

There is a tricky solution, though:7 We can declare a copy constructor for const volatile arguments and mark it “deleted” (i.e., define it with = delete). Doing so prevents another copy constructor from being implicitly declared. With that in place, we can define a constructor template that will be preferred over the (deleted) copy constructor for nonvolatile types:

class C
{
  public:
    
    // user-define the predefined copy constructor as deleted
    // (with conversion to volatile to enable better matches)
    C(C const volatile&) = delete;

    // implement copy constructor template with better match:
    template<typename T>
    C (T const&) {
        std::cout << "tmpl copy constructor ";
    }
    
};

Now the template constructors are used even for “normal” copying:

C x;
C y{x};  // uses the member template

In such a template constructor we can then apply additional constraints with enable_if<>. For example, to prevent being able to copy objects of a class template C<> if the template parameter is an integral type, we can implement the following:

template<typename T>
class C
{
  public:
    
    // user-define the predefined copy constructor as deleted
    // (with conversion to volatile to enable better matches)
    C(C const volatile&) = delete;

    // if T is no integral type, provide copy constructor template with better match:
    template<typename U,
             typename = std::enable_if_t<!std::is_integral<U>::value>>
    C (C<U> const&) {
        
    }
    
};

6.5 Using Concepts to Simplify enable_if<> Expressions

Even when using alias templates, the enable_if syntax is pretty clumsy, because it uses a work-around: To get the desired effect, we add an additional template parameter and “abuse” that parameter to provide a specific requirement for the function template to be available at all. Code like this is hard to read and makes the rest of the function template hard to understand.

In principle, we just need a language feature that allows us to formulate requirements or constraints for a function in a way that causes the function to be ignored if the requirements/constraints are not met.

This is an application of the long-awaited language feature concepts, which allows us to formulate requirements/conditions for templates with its own simple syntax. Unfortunately, although long discussed, concepts still did not become part of the C++17 standard. Some compilers provide experimental support for such a feature, however, and concepts will likely become part of the next standard after C++17.

With concepts, as their use is proposed, we simply have to write the following:

template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n) : name(std::forward<STR>(n)) {
    …
}

We can even specify the requirement as a general concept

template<typename T>
concept ConvertibleToString = std::is_convertible_v<T,std::string>;

and formulate this concept as a requirement:

template<typename STR>
requires ConvertibleToString<STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
    …
}

This also can be formulated as follows:

template<ConvertibleToString STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
    …
}

See Appendix E for a detailed discussion of concepts for C++.

6.6 Summary

• In templates, you can “perfectly” forward parameters by declaring them as forwarding references (declared with a type formed with the name of a template parameter followed by &&) and using std::forward<>() in the forwarded call.

• When using perfect forwarding member function templates, they might match better than the predefined special member function to copy or move objects.

• With std::enable_if<>, you can disable a function template when a compile-time condition is false (the template is then ignored once that condition has been determined).

• By using std::enable_if<> you can avoid problems when constructor templates or assignment operator templates that can be called for single arguments are a better match than implicitly generated special member functions.

• You can templify (and apply enable_if<>) to special member functions by deleting the predefined special member functions for const volatile.

• Concepts will allow us to use a more intuitive syntax for requirements on function templates.

1 The fact that move semantics is not automatically passed through is intentional and important. If it weren’t, we would lose the value of a movable object the first time we use it in a function.

2 A type like X const&& is valid but provides no common semantics in practice because “stealing” the internal representation of a movable object requires modifying that object. It might be used, though, to force passing only temporaries or objects marked with std::move() without being able to modify them.

3 The term universal reference was coined by Scott Meyers as a common term that could result in either an “lvalue reference” or an “rvalue reference.” Because “universal” was, well, too universal, the C++17 standard introduced the term forwarding reference, because the major reason to use such a reference is to forward objects. However, note that it does not automatically forward. The term does not describe what it is but what it is typically used for.

4 Don’t forget to place the condition into parentheses, because otherwise the > in the condition would end the template argument list.

5 Thanks to Stephen C. Dewhurst for pointing that out.

6 If you wonder why we don’t instead check whether STR is “not convertible to Person,” beware: We are defining a function that might allow us to convert a string to a Person. So the constructor has to know whether it is enabled, which depends on whether it is convertible, which depends on whether it is enabled, and so on. Never use enable_if in places that impact the condition used by enable_if. This is a logical error that compilers do not necessarily detect.

7 Thanks to Peter Dimov for pointing out this technique.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.252.140