Недавно посмотрел курс Advanced C++ Programming на портале caveofprogramming.com. Как обычно, решил некоторые вещи законспектировать, чтобы не забыть.
- Определение пользовательского типа внутри функции
- Ключевое слово auto
- Range-based for loop
- Вложенные классы
- Что должен уметь класс контейнера, чтобы быть «итерабельным»
- Лямбда-выражения
- std::function — General-purpose polymorphic function wrapper
- Шаблоны с произвольным числом аргументов (Variadic Templates)
- Rvalue reference
- Perfect forwarding
- Способы передачи параметров в функции
- std::bind
- static_assert
- Что осталось за кадром
Литература
- Дэвид Вандервурд, Николаи Джосаттис, Дуглас Грегор — Шаблоны C++. Справочник разработчика. 2-е издание
- Scott Meyers — Effective Modern C++
- Nicolai Josuttis — The C++ Standard Library. A Tutorial and Reference
Определение пользовательского типа внутри функции
Начнем с мелочей. Я не знал, что так можно, но вот оказывается, да:
{
// declaring user type within function body!
class Test
{
public:
int x;
int y;
};
Test tst{ 10, 20 };
cout << tst.x << " " << tst.y;
cin.get();
}
Можно определять пользовательские типы прямо внутри блока, но это не должны быть шаблонные типы [Вандервурд].
Ключевое слово auto
Я уже был знаком с эти словом и использовал его точно так же как ключевое слово var в C# — чтобы не указывать тип переменной при ее объявлении и инициализации:
auto pVec = new std::vector<int>(); // the same as std::vector* pVec = new std::vector();
Особенно полезно это слово бывает, когда имя типа длинное:
Чего я не знал, так это, что ключевое слово auto может быть использовано при объявлении функции в сочетании с ключевым словом decltype. Для обычной функции в этом быть может мало смысла, но может пригодиться для шаблонной функции:
auto add(T1 a, T2 b) -> decltype(a + b)
{
return a + b;
}
int main()
{
std::cout << add(1, 3.14) << std::endl; // calls add<int, double>()
std::cin.get();
}
Вот эта штука -> decltype(a + b)
называется trailing return type. Слово auto здесь говорит о том, что тип возвращаемого значения функции — это trailing return type. В C++14 тип возвращаемого значения уже может быть определен компилятором автоматически, т. е. trailing return type указывать не нужно (но это возможно, только если объявление функции совмещено с ее определением):
auto add(T1 a, T2 b)
{
return a + b;
}
В книге [Вандервурд, раздел 1.3.2. Вывод возвращаемого типа] объясняется, что если возвращаемый тип может оказаться ссылкой, то следует в trailing return type применить шаблон std::decay<T>::type
, который «низводит» ссылку до значения (кстати, то же самое делает ключевое слово auto). Также можно применить шаблон std::common_type<T1, T2>::type
, который вычисляет общий тип для T1 и T2:
typename std::common_type<T1, T2>::type add(T1 a, T2 b)
{
return a + b;
}
Range-based for loop
Чтобы перебрать все элементы контейнера в C++98 надо было написать цикл вроде следующего:
{
for (vector<int>::iterator i = vec.begin(); i != vec.end(); i++)
cout << *i << endl;
}
В C++11 такой же код можно записать короче:
{
for (auto x : vec)
cout << x << endl;
}
Приведенный код годится для коллекции чисел. А вот для коллекции более крупных объектов лучше использовать другой код (см. [Josuttis, 3.1.4 Range-Based for Loops]):
void print_vector(vector<MyType>& vec)
{
for (const auto& x : vec)
cout << x << endl;
}
Почему? Потому что в приведенном коде мы имеем дело с константными ссылками на элементы вектора, и поэтому копирования элементов вектора не происходит. А в коде for (auto x : vec) { ... }
происходит копирование элементов вектора, мы имеем дело с их копиями, а ненужного копирования всегда стараются избегать.
Если вам нужно изменять элементы списка, то вы тоже должны использовать ссылки, иначе вы измените копии элементов, а не сами элементы:
{
/*
// The following code is wrong because it changes the copies of elements!
for (auto x : vec)
++x;
*/
for (auto& x : vec)
++x;
}
Вложенные классы
Вложенный класс имеет доступ ко всем членам того класса, в который он вложен. Пример вложенного класса, который сразу приходит в голову: класс любого стандартного контейнера STL имеет вложенный класс iterator. Если вы захотите создать свой класс контейнера my_awesome_container<T>, то вы наверняка должны будете создать вложенный класс итератора. Написать это можно так:
class my_awesome_container
{
public:
// Nested type - iterator
class iterator
{
// members of iterator
};
// ... other members of the class
};
… или можно «разделить два класса в пространстве»:
class my_awesome_container
{
public:
class iterator;
// ... other members of the class
};
template<typename T>
class my_awesome_container<T>::iterator
{
// members of iterator
};
Что должен уметь класс контейнера, чтобы быть «итерабельным»
В курсе от caveofprogramming.com автор рассказывает как реализовать контейнер, чтобы он был итерабельным, т. е. чтобы можно было перебрать его элементы при помощи range-based for loop. Он приводит в качестве примера нечто подобное (я изменил код, чтобы не нарушать чужих авторских прав):
using namespace std;
template<typename T>
class my_awesome_container
{
private:
T* m_internal_array;
int m_size;
public:
class iterator;
public:
my_awesome_container(int size) :
m_internal_array(new T[size]{}),
m_size(size)
{ }
~my_awesome_container() { delete[] m_internal_array; }
int size() const { return m_size; }
T& operator[](int index) { return m_internal_array[index]; }
iterator begin() { return iterator(*this, 0); }
iterator end() { return iterator(*this, m_size); }
};
template<typename T>
class my_awesome_container<T>::iterator
{
private:
my_awesome_container& container;
int index;
public:
iterator(my_awesome_container& container, int index) :
container(container),
index(index)
{ }
// dereference operator
T& operator*() { return container[index]; }
// inequality operator
bool operator!=(const iterator& rhs) { return index != rhs.index; }
// postincrement operator
iterator& operator++(int)
{
return iterator(container, index++);
}
// preincrement operator
iterator& operator++()
{
++index;
return *this;
}
};
void main()
{
my_awesome_container<int> vec(5);
// populate container
for (int i = 0; i < vec.size(); i++)
vec[i] = i;
// iterate through the container
for (auto x : vec)
cout << x << endl;
cin.get();
}
Хотя приведенный выше код контейнера и итератора работает, он далеко не полноценен. Если вы попробуете применить к такому контейнеру какой-нибудь стандартный алгоритм (например, find), то получите ошибку компиляции. Это из-за того, что приведенный итератор реализует не все требования, предъявляемые к итераторам. На страничке cplusplus.com/reference/iterator можно посмотреть полный список требований к итераторам различных категорий. Руководствуясь этими требованиями, я дополнил код итератора, чтобы он соответствовал категории forward iterator, после чего и алгоритм find заработал:
#include <algorithm>
using namespace std;
template<typename T>
class my_awesome_container
{
private:
T* m_internal_array;
int m_size;
public:
class iterator;
public:
my_awesome_container(int size) :
m_internal_array(new T[size]{}),
m_size(size)
{ }
~my_awesome_container()
{
delete[] m_internal_array;
}
int size() const
{
return m_size;
}
T& operator[](int index)
{
return m_internal_array[index];
}
iterator begin()
{
return iterator(*this, 0);
}
iterator end()
{
return iterator();
}
};
template<typename T>
class my_awesome_container<T>::iterator : public std::iterator<forward_iterator_tag, T>
{
private:
my_awesome_container* container = nullptr;
int index = 0;
public:
iterator() = default;
iterator(const iterator& rhs) = default;
iterator(my_awesome_container& container, int index) :
container(&container),
index(index)
{ }
const iterator& operator=(const iterator& rhs)
{
this->container = rhs.container;
this->index = rhs.index;
return *this;
}
T& operator*()
{
return container->operator[](index);
}
T* operator->()
{
return &container->operator[](index);
}
bool operator==(const iterator& rhs)
{
return (this->container == rhs.container && index == rhs.index);
}
bool operator!=(const iterator& rhs)
{
return !(*this == rhs);
}
// postincrement
iterator& operator++(int)
{
iterator ret_value = iterator(*container, index++);
if (index >= container->size())
{
container = nullptr;
index = 0;
}
return ret_value;
}
// preincrement
iterator& operator++()
{
if (++index >= container->size())
{
container = nullptr;
index = 0;
}
return *this;
}
};
void main()
{
my_awesome_container<int> vec(10);
for (int i = 0; i < vec.size(); i++)
vec[i] = i;
auto searched_iter = find(vec.begin(), vec.end(), 5);
if(searched_iter != vec.end())
cout << *searched_iter << endl;
cin.get();
}
Что изменилось в классе итератора. Во-первых, класс итератора наследуется от шаблонного класса std::iterator, в одном из параметров которого в частности указывается к какой категории принадлежит наш итератор. Во-вторых, добавились конструктор по-умолчанию и конструктор копирования. В-третьих, добавились операторы присваивания (=), разыменования (->) и сравнения (==).
Лямбда-выражения
Лямбда выражения знакомы мне по C# — это способ создать анонимную функцию с целью передачи указателя на нее куда-либо (например в другую функцию). В C++ лямбда выражения обеспечивают удобство работы с алгоритмами стандартной библиотеки C++, заменяя собой более трудоемкие в написании функторы. Чтобы объявить функцию, надо указать ее возвращаемое значение и список параметров. Функторы являются объектами, и поэтому помимо параметров и возвращаемого значения могут еще и обладать состоянием, т. е., грубо говоря, иметь поля. Лямбда-выражения тоже являются объектами, а в качестве состояния могут хранить захваченные локальные переменные (captured local variables) из той функции, в которой объявлено лямбда-выражение. Причем это могут быть как значения переменных, так и ссылки на них. Параметры в лямбда-выражениях указываются в круглых скобках. Тело выражения указывается в фигурных скобках. Захваченные переменные указываются в квадратных скобках (а вот например в C# захваченные значения указывать нигде не нужно). В C++ есть синтаксис на случай, если надо захватить все локальные переменные сразу по значению или по ссылке (см. примеры ниже). Приведу несколько примеров различных лямбда-выражений:
#include <string>
using namespace std;
void main()
{
auto lambda1 = [](){}; // the simplest lambda: no return value (void), no captured variables, no params, no code
lambda1(); // calling lambda-expression
// Another simple lambda: no return value, no captured variables, no params
auto lambda2 = []() { cout << "lambda2: Hello" << endl; };
lambda2();
// Return type is specified explicitly (-> double)
auto lambda3 = [](double a, int b) -> double { return a + b; };
cout << "lambda3: " << lambda3(4.5, 3) << endl;
auto lambda4 = [](int a, int b) { return a + b; };
cout << "lambda4: " << lambda4(4, 3) << endl;
// declaring local variables in order to show capturing in lambda-expressions
int var1 = 5;
float var2 = 1.5f;
// Captures specified variables by value
auto lambda5 = [var1, var2](string str_param) { cout << str_param << var1 << " " << var2 << endl; };
lambda5("lambda5: captured variables: ");
// Captures all local variables by value
auto lambda6 = [=](string str_param) { cout << str_param << var1 << " " << var2 << endl; };
lambda6("lambda6: captured variables: ");
// Captures all local variables by reference
auto lambda7 = [&](string str_param) { ++var1; --var2; cout << str_param << var1 << " " << var2 << endl; };
lambda7("lambda7: captured variables: ");
// Captures all local variables by reference except var2, which is captured by value
auto lambda8 = [&, var2](string str_param) { ++var1; cout << str_param << var1 << " " << var2 << endl; };
lambda8("lambda8: captured variables: ");
// Compilation error: var2 is captured by value, so it cannot be modified
//auto lambda8_1 = [&, var2](string str_param) { ++var1; /*Error->*/ --var2; cout << str_param << var1 << " " << var2 << endl; };
// Captures var1 by reference and var2 by value
auto lambda9 = [&var1, var2](string str_param) { ++var1; cout << str_param << var1 << " " << var2 << endl; };
lambda9("lambda9: captured variables: ");
// Captures all local variables by value except var2, which is captured by reference
auto lambda10 = [=, &var2](string str_param) { ++var2; cout << str_param << var1 << " " << var2 << endl; };
lambda10("lambda10: captured variables: ");
cin.get();
}
Когда лямбда выражение определяется внутри функции-члена класса, бывает необходимо, чтобы оно захватывало не только локальные переменные, но и указатель this:
#include <string>
using namespace std;
class TestLambda
{
private:
int m_Var1 = 3;
float m_Var2 = 1.2f;
public:
void foo()
{
int var1 = 5;
float var2 = 1.5f;
// Captures this pointer. Also captures var1 by value and var2 by reference
auto lambda1 = [this, var1, &var2](string str_param)
{
m_Var1++;
var2--;
cout << str_param << m_Var1 << " " << m_Var2 << " " << var1 << " " << var2 << endl;
};
lambda1("lambda1: captured variables: ");
// Trying to capture this and to capture all local variables by value leads to a compilation error
/*
auto lambda2 = [=, this](string str_param)
// _____________^__^______________Error!!!
{
cout << str_param << m_Var1 << " " << m_Var2 << " " << var1 << " " << var2 << endl;
};
*/
// Captures this pointer. Also captures all local variables by reference
auto lambda3 = [&, this](string str_param)
{
m_Var1++;
var2++;
cout << str_param << m_Var1 << " " << m_Var2 << " " << var1 << " " << var2 << endl;
};
lambda3("lambda3: captured variables: ");
}
};
void main()
{
TestLambda testLambda;
testLambda.foo();
cin.get();
}
std::function — General-purpose polymorphic function wrapper
Шаблонный класс std::function представляет собой универсальный указатель на любой объект, который может быть вызван как функция. Вызваны, насколько мне известно, могут быть три вида объектов: указатели на функции, функторы (объекты, для которых переопределен оператор «круглые скобки» operator()), и лямбда-выражения (которые, если я не ошибаюсь, являются разновидностью функторов). Так вот, любой из этих объектов может быть преобразован к типу std::function. (Программисту на языке C# сразу вспоминаются делегаты). Посмотрим, как работает std::function:
#include <functional>
using namespace std;
// this function accepts function pointers, functors as well as lambda-expressions
void run(function<bool(int)> func, int arg)
{
cout << func(arg) << endl;
}
// function
bool greater_than_zero(int arg)
{
return arg > 0;
}
// functor
class less_than_zero
{
public:
bool operator()(int arg)
{
return arg < 0;
}
};
void main()
{
// calls function
run(greater_than_zero, 5); // prints: 1
// calls lambda-expression
run([](int arg) { return arg % 2 == 0; }, 4); // prints: 1
// calls functor
run(less_than_zero(), -3); // prints: 1
cin.get();
}
Чтобы понять, как в принципе мог бы быть реализован класс std::function, я написал аналогичный класс, который правда подходит для вызова только с одним аргументом:
#include <functional>
using namespace std;
template<typename TRet, typename TArg>
class single_argument_function
{
private:
TRet(*function_pointer)(TArg); // function pointer
class stub_class { };
stub_class* functor_object_pointer; // pointer to a functor object
TRet(__thiscall stub_class::* functor_operator_parentheses)(TArg); // pointer to operator() of the functor object
public:
// Constructor taking function pointer as argument
single_argument_function(TRet(*function_pointer)(TArg)) :
function_pointer(function_pointer),
functor_object_pointer(nullptr),
functor_operator_parentheses(nullptr)
{ }
// Constructor taking a pointer to functor object as argument
template<typename T>
single_argument_function(T obj) :
function_pointer(nullptr),
functor_object_pointer(reinterpret_cast<stub_class*>(&obj)),
functor_operator_parentheses(reinterpret_cast<TRet(__thiscall stub_class::*)(TArg)>(&T::operator()))
{ }
// operator()
TRet operator()(TArg arg)
{
if (functor_operator_parentheses != nullptr)
return (functor_object_pointer->*functor_operator_parentheses)(arg);
if (function_pointer != nullptr)
return function_pointer(arg);
}
};
// this function accepts function pointers, functors as well as lambda-expressions
void run(single_argument_function<bool, int> func, int arg)
{
cout << func(arg) << endl;
}
// function
bool greater_than_zero(int arg)
{
return arg > 0;
}
// functor
class less_than_zero
{
public:
bool operator()(int arg)
{
return arg < 0;
}
};
void main()
{
// calls function
run(greater_than_zero, 5); // prints: 1
// calls lambda-expression
run([](int arg) { return arg % 2 == 0; }, 4); // prints: 1
// calls functor
run(less_than_zero{}, -3); // prints: 1
cin.get();
}
Приведенный код дался мне это нелегко. Оказывается, для того, чтобы вызвать указатель на метод класса, компилятору непременно надо знать, что это за класс. Казалось бы, достаточно запихнуть в регистр ecx адрес объекта (this), и выполнить машинную команду call с адресом метода в качестве аргумента; и не нужно вам знать, к какому классу принадлежит объект. Но нет, компилятор желает знать класс. Хорошо, пришлось дать ему пустой класс-заглушку class stub_class { };
.
Шаблоны с произвольным числом аргументов (Variadic Templates)
Приведенный выше код работает только для функций с одним аргументом. Чтобы сделать его более универсальным, нужно воспользоваться новой фишкой C++11 под названием variadic templates [Вандервурд, Глава 4 — Вариативные шаблоны].
#include <functional>
using namespace std;
// The class representing any callable object with variable number of arguments.
template<typename TRet, typename... TArgs>
class multiple_argument_function
{
private:
TRet(*function_pointer)(TArgs...); // function pointer
class stub_class { };
stub_class* functor_object_pointer; // pointer to a functor object
TRet(__thiscall stub_class::* functor_operator_parentheses)(TArgs...); // pointer to operator() of the functor object
public:
// Constructor taking function pointer as argument
multiple_argument_function(TRet(*function_pointer)(TArgs...)) :
function_pointer(function_pointer),
functor_object_pointer(nullptr),
functor_operator_parentheses(nullptr)
{ }
// Constructor taking a pointer to functor object as argument
template<typename T>
multiple_argument_function(T obj) :
function_pointer(nullptr),
functor_object_pointer(reinterpret_cast<stub_class*>(&obj)),
functor_operator_parentheses(reinterpret_cast<TRet(__thiscall stub_class::*)(TArgs...)>(&T::operator()))
{ }
// operator()
TRet operator()(TArgs... args)
{
if (functor_operator_parentheses != nullptr)
return (functor_object_pointer->*functor_operator_parentheses)(args...);
if (function_pointer != nullptr)
return function_pointer(args...);
}
};
// this variadic function template accepts function pointers, functors as well as lambda-expressions
template<typename TFunc, typename... TArgs>
void run(TFunc func, TArgs... args)
{
cout << func(args...) << endl;
}
// function
double sum(int a, float b, double c)
{
cout << a << " + " << b << " + " << c << " = " << endl;
return a + b + c;
}
// functor
class sum_functor
{
public:
double operator()(int a, float b, double c)
{
cout << a << " + " << b << " + " << c << " = " << endl;
return a + b + c;
}
};
void main()
{
// calls function
run(sum, 1, 2.5f, 4.3);
// calls lambda-expression
run([](int a, float b, double c) -> double
{
cout << a << " + " << b << " + " << c << " = " << endl;
return a + b + c;
},
1, 2.5f, 4.3);
// calls functor
run(sum_functor(), 1, 2.5f, 4.3);
cin.get();
}
Rvalue reference
В стандарте C++11 появилась возможность отделить два понятия: lvalue и rvalue. lvalue — это значение, которое может находиться слева от оператора присваивания (и справа тоже). rvalue — это значение, которое не может находиться слева от оператора присваивания, а может находиться только справа. Посмотрим на примеры того и другого:
a = 5; // a - lvalue; 5 - rvalue
int c = cin.get(); // c - lvalue; the return value of cin.get() is an rvalue
lvalue — это переменная или параметр функции. rvalue — это или литерал, или возвращаемое значение функции (если функция возвращает именно значение, а не ссылку на переменную).
Необходимость разделения понятий lvalue и rvalue, как я понимаю, возникает чтобы избежать излишнего копирования объектов. Копирование объектов происходит при передаче параметров в функции по значению, при инициализации объектов (вызов копирующего конструктора), при присваивании (вызов оператора копирующего присваивания). А копирование бывает излишним именно тогда, когда в функцию передаются по значению значения rvalue, когда объект инициализируется значением rvalue (rvalue передается в копирующий конструктор), когда переменной присваивается rvalue (в оператор присваивания передается rvalue). Почему копирование излишне? Потому, что rvalue после этого копирования уничтожается, т. е. вызывается деструктор, который освобождает все захваченные объектом ресурсы. Вместо того, чтобы копировать ресурсы (а копирование ресурсов может быть очень ресурсозатратной операцией) из одного объекта в другой, а затем уничтожать одну их копий, можно просто передать ресурсы одного объекта другому без всякого копирования. Вот поэтому и появилось в стандарте понятие rvalue reference. Обозначается rvalue-ссылка на тип T как T&&. Смысл это понятие имеет, как я понимаю, только как тип параметра функции. Можно использовать rvalue reference и как тип локальной переменной, но в этом я не вижу никакого смысла, поскольку в таком качестве rvalue reference ведет себя точно так же как lvalue reference.
Посмотрим как компилятор различает ссылки lvalue и rvalue. В примере ниже есть две перегруженных функции, одна из которых принимает в качестве параметра lvalue reference, а другая — rvalue reference:
void print_int(int& a) { cout << "lvalue " << a << endl; }
// function accepts rvalue reference
void print_int(int&& a) { cout << "rvalue " << a << endl; }
// function return value is an rvalue
int foo() { return 3; }
int x = 4;
// function return value is an lvalue
int& bar() { return x; }
int main()
{
int a = 1;
print_int(a); // lvalue 1
print_int(2); // rvalue 2
print_int(foo()); // rvalue 3
print_int(bar()); // lvalue 4
cin.get();
}
Но это только иллюстрация того, что компилятор различает lvalue и rvalue. Практическое применение этой способности компилятора заключается в создании конструкторов перемещения (moving constructor) и перемещающих операторов присваивания (moving assignment operator). Чтобы это продемонстрировать, вернемся к классу my_awesome_container:
class my_awesome_container
{
private:
T* m_internal_array;
int m_size;
public:
// Moving constructor
my_awesome_container(my_awesome_container&& rhs) :
m_internal_array(rhs.m_internal_array),
m_size(rhs.m_size)
{
rhs.m_internal_array = nullptr;
rhs.m_size = 0;
}
// Moving assignment operator
const my_awesome_container& operator=(my_awesome_container&& rhs)
{
m_internal_array = rhs.m_internal_array;
m_size = rhs.m_size;
rhs.m_internal_array = nullptr;
rhs.m_size = 0;
return *this;
}
// Constructor with parameters
my_awesome_container(int size) :
m_internal_array(new T[size]{}),
m_size(size)
{ }
~my_awesome_container()
{
delete[] m_internal_array; // Note: deleting nullptr is ok now
}
// ... other members of the class
}
my_awesome_container make_my_awesome_container(int size)
{
return my_awesome_container(size);
}
void main()
{
my_awesome_container vec1
= make_my_awesome_container(10); // calls moving constructor
my_awesome_container vec2(5); // calls constructor with parameters
vec2 = make_my_awesome_container(7); // calls moving assignment operator
vec1 = vec2; // calls copy assignment operator
vec1 = std::move(vec2); // calls moving assignment operator
my_awesome_container vec3 = vec1; // calls copy constructor
cin.get();
}
Ну и наконец, поговорим о бессмысленных (Это только на мой взгляд. Может я чего не понимаю) операциях с rvalue-ссылками. Это — объявление локальной переменной типа rvalue-reference, а оно, как показывает эксперимент, эквивалентно объявлению локальной переменной типа lvalue-reference:
Test make_Test() { return Test(); }
void print_Test(const Test& t) { cout << "print lvalue" << endl; }
void print_Test(Test&& t) { cout << "print rvalue" << endl; }
int main()
{
Test lvalue_tst; // Local variable declaration
// Test&& rvalue_tst = lvalue_tst; // Compilation error (rvalue reference cannot be bound to an lvalue)
Test&& rvalue_tst = make_Test(); // Fine: rvalue reference is bound to an rvalue. But this is equivalent to Test& rvalue_tst = make_Test(1, 2);
print_Test(lvalue_tst); // print lvalue
print_Test(rvalue_tst); // print lvalue
cin.get();
}
Perfect forwarding
Допустим у нас есть перегруженная функция foo, одна версия которой принимает в качестве параметра rvalue, а другая lvalue:
#include <iostream>
using namespace std;
class Test
{
public:
int val;
public:
// Constructor with parameters
Test(int val) : val(val)
{
cout << "constructor: " << val << endl;
}
// Copy constructor
Test(const Test& rhs) : val(rhs.val)
{
cout << "copy constructor: " << val << endl;
}
// Destructor
~Test()
{
cout << "destructor: " << val << endl;
}
};
void foo(Test& t)
{
cout << "lvalue" << endl;
}
void foo(Test&& t)
{
cout << "rvalue" << endl;
}
void no_forwarding(Test&& t)
{
foo(t);
}
void main()
{
no_forwarding(Test(1)); // prints: rvalue or lvalue???
}
Угадайте, что выведет на экран программа, rvalue или lvalue? Думаете rvalue? Нет, она выведет lvalue. Как так? А дело в том, что Test&& t — это не rvalue, а lvalue, хотя тип параметра t — это rvalue reference. Но rvalue reference вовсе необязательно является rvalue. Если я вас совсем запутал, то почитайте лучше [Meyers, Chapter 5. Rvalue References, Move Semantics and Perfect Forwarding]. Является ли поведение функции no_forwarding желательным для нас? Конечно нет, так как объект rvalue рассматривается функцией foo как объект lvalue, что потенциально может приводить к излишнему копированию этого объекта. Исправить это поведение можно разными способами, и в книжке [Meyers] рассказано, почему единственно верный из них — это использование функции std::forward. Если вкратце, то функция std::forward «возвращает» параметру его «правоссылковость» (rvalueness) в том случае, если это параметр является rvalue-ссылкой:
{
cout << "lvalue" << endl;
}
void foo(Test&& t)
{
cout << "rvalue" << endl;
}
template<typename T>
void perf_forwarding_test(T&& t)
{
foo(forward<T>(t)); // std::forward preserves l- or r-valueness of the argument
// foo(static_cast<T&&>(t)); // alternative way. does the same thing.
}
/*
// template specialization of std::forward for lvalue reference parameter
template<class T>
constexpr T&& forward(remove_reference_t<T>& arg)
{
return (static_cast<T&&>(arg));
}
// template specialization of std::forward for rvalue reference parameter
template<class T>
constexpr T&& forward(remove_reference_t<T>&& arg)
{
return (static_cast<T&&>(arg));
}
*/
void main()
{
Test test(1);
// Instantiates perf_forwarding_test<Test&>(Test& t)
// Reference collapsing is taking place: perf_forwarding_test<Test&>(Test&&& t) -> perf_forwarding_test<Test&>(Test& t)
// forward<Test&>(t) -> static_cast<Test&&&>(t) -> static_cast<Test&>(t)
perf_forwarding_test(test); // prints: lvalue
// the same as above. we just specified template parameter explicitly
perf_forwarding_test<Test&>(test); // prints: lvalue
// Instantiates perf_forwarding_test<Test>(Test&& t)
// forward<Test>(t) -> static_cast<Test&&>(t) -> the same as static_cast<Test>(t)
perf_forwarding_test(Test(2)); // prints: rvalue
// the same as above. we just specified template parameter explicitly
perf_forwarding_test<Test>(Test(3)); // prints: rvalue
// compilation error: perf_forwarding_test<Test> requires parameter of type Test&&, while test is of type Test&
// perf_forwarding_test<Test>(test);
// compilation error: perf_forwarding_test<Test&> requires parameter of type Test&&&=Test&, while Test(3) is of type Test&&
// perf_forwarding_test<Test&>(Test(4));
cin.get();
}
Автор курса caveofprogramming.com говорит, что вызов функции std::forward<T>(arg) эквивалентен static_cast<T>(arg). Давайте разберемся, почему он неправ. Рассмотрим такой код:
{
cout << "lvalue" << endl;
}
void foo(Test&& t)
{
cout << "rvalue" << endl;
}
template<typename T>
void wrong_perf_forwarding(T&& t)
{
foo(static_cast<T>(t));
}
void main()
{
Test test(1);
wrong_perf_forwarding(test);
/*
test is an lvalue => T=Test&
wrong_perf_forwarding<Test&>(test);
template<typename T = Test&>
void wrong_perf_forwarding(Test&&& t = collapses to Test& t = lvalue reference)
{
// reference casts to reference, no copying, prints: lvalue
foo(static_cast<Test&>(t));
}
FINE, NO COPYING
*/
wrong_perf_forwarding(Test(2));
/*
Test(2) is an rvalue => T=Test
wrong_perf_forwarding<Test>(Test(2));
template<typename T = Test>
void wrong_perf_forwarding(Test&& t = rvalue reference)
{
// cast to Test leads to copying, static_cast<Test>(t) is an rvalue, so prints: rvalue
foo(static_cast<Test>(t));
}
BAD, COPYING HAPPENS
*/
cin.get();
}
При передаче rvalue в качестве параметра происходит ненужное копирование (пояснения даны в коде).
Чтобы как следует разобраться во всех этих взрывающих мозг rvalue reference, lvalue reference и forwarding reference, читайте книжку [Meyers, Chapter 5. Rvalue References, Move Semantics and Perfect Forwarding]. Пока я все это читал, у меня появились еще вопросы «а что будет если…?» касательно параметров функций типа rvalue-reference и lvalue-reference а также слова auto, которые я разрешил при помощи эксперимента. Код привожу ниже:
using namespace std;
ostream& operator<<(ostream& os, Test& t)
{
return os << t.val << endl;
}
// Accepts rvalues only
void foo(Test&& t)
{
cout << "foo: " << t;
}
// Accepts both lvalues and rvalues
void bar(Test& t)
{
cout << "bar: " << t;
}
void main()
{
Test t1(1); // invokes constructor with parameters
// *** Can I call a function, taking rvalue reference with lvalue? ***
// foo(t1); // compilation error: rvalue reference cannot be bound to an lvalue
foo(Test(1)); // fine
foo(std::move(t1)); // fine: casts lvalue reference to rvalue reference
foo(static_cast<Test&&>(t1)); // fine: does the same thing as std::move()
bar(t1); // fine
bar(Test(1)); // fine
// *** When I declare and initialize a variable at what conditions copying takes place? ***
Test t2 = Test(2); // invokes constructor with parameters
cout << t2;
auto t3 = Test(3); // invokes constructor with parameters
cout << t3;
auto& t4 = Test(4); // invokes constructor with parameters
cout << t4;
auto t5 = t1; // invokes copy constructor
cout << t5;
auto& t6 = t1; // inits a reference to t1; t6 is of type Test&
cout << t6;
auto&& t7 = t1; // inits a reference to t1; t7 is of type Test&
cout << t7;
auto&& t8 = Test(8); // t8 is of type Test&&; the same as Test t8 = Test(8);
cout << t8;
// foo(t8); // compilation error: rvalue reference cannot be bound to an lvalue
Test&& t9 = Test(9); // t9 is of type Test&&; the same as Test t9 = Test(9);
cout << t9;
// foo(t9); // compilation error: rvalue reference cannot be bound to an lvalue
cin.get();
}
Поскольку книжку Мейерса я читал не последовательно, я только позже обнаружил, что он о выведении типов (type deduction) при использовании слова auto пишет подробно в главе 1 — Deducing Types.
Способы передачи параметров в функции
Хотелось бы, резюмируя, перечислить способы передачи параметров в функции, коих в C++11 прибавилось:
void foo1(MyType arg); // (С++98) function accepting parameter by value (copying takes place)
void foo2(MyType& arg); // (С++98) function accepting parameter by reference
void foo3(const MyType& arg); // (С++98) function accepting parameter by constant reference
void foo4(MyType&& arg); // (С++11) function accepting rvalues only (by reference)
// (С++11) function accepting a "universal reference" (both rvalue reference and lvalue reference)
template<typename T>
void foo5(T&& arg);
void main()
{
foo5(MyType()); // instantiates foo5<MyType>(MyType&&)
MyType mt;
foo5(mt); // instantiates foo5<MyType&>(MyType&)
// This will not compile. You cannot pass rvalue by nonconstant reference.
//foo2(MyType());
// But you can pass rvalue by constant reference.
foo3(MyType());
}
Несколько неожиданным для меня оказалось то, что оказывается нельзя передать rvalue по неконстантной ссылке, а вот по константной ссылке — можно.
std::bind
Функция std::bind возвращает объект, который может быть вызван как функция (в классе, к которому этот объект принадлежит, переопределен operator()), т. е. он является функтором. В качестве параметра std::bind принимает указатель на функцию либо функтор, плюс значения всех или некоторых параметров, которые могут быть переданы этой функции или этому функтору. Некоторые из параметров могут быть заменены плейсхолдерами (_1, _2, _3 и т. д.). Словесное объяснение звучит запутанно, надеюсь, из примеров ниже все станет ясно:
#include <functional>
using namespace std;
using namespace std::placeholders;
int multiply(int a, int b, int c)
{
cout << a << " * " << b << " * " << c << " = ";
return a * b * c;
}
void call_function(function<int(int, int)> func)
{
cout << func(1, 2) << endl;
}
class MyFunctor
{
public:
int operator()(int a, int b, int c)
{
cout << a << " * " << b << " * " << c << " = ";
return a * b * c;
}
};
void main()
{
auto bind1 = bind(multiply, 1, 2, 3);
cout << bind1() << endl; // prints: 1 * 2 * 3 = 6
auto bind2 = bind(multiply, _1, _2, _3);
cout << bind2(1, 2, 3) << endl; // prints: 1 * 2 * 3 = 6
auto bind3 = bind(multiply, _1, 2, 3);
cout << bind3(1) << endl; // prints: 1 * 2 * 3 = 6
auto bind4 = bind(multiply, _2, _1, 3);
cout << bind4(1, 2) << endl; // prints: 2 * 1 * 3 = 6
call_function(bind4); // prints: 2 * 1 * 3 = 6
MyFunctor my_functor;
auto bind5 = bind(my_functor, _2, _1, 3);
cout << bind5(1, 2) << endl; // prints: 2 * 1 * 3 = 6
cin.get();
}
Обратите внимание, что плейсхолдеры принадлежат к пространству имен std::placeholders (std::placeholders::_1, std::placeholders::_2) и т. д.
static_assert
Ключевое слово, которое позволяет делать различные проверки (assertions) на этапе компиляции. Проверки обычно касаются шаблонных параметров классов и функций. Например, предположим, вы пишете шаблонный класс для представления точки на плоскости Point<T>, где T — это параметр шаблона, которые определяет тип координаты X и координаты Y. Вы вероятно хотели бы ограничить возможные значения параметра T числовыми типами. Если программист попытается в качестве параметра шаблона использовать нечисловой тип, то должна выбрасываться ошибка компиляции. Тогда вам просто надо написать в теле класса static_assert:
template<typename T>
class Point
{
static_assert(std::is_arithmetic<T>::value, "Template parameter T must be a numeric type!");
public:
T X;
T Y;
};
1-ым аргументом static_assert является булевская константа, значение которой определяется во время компиляции (constexpr — см. ниже). Если константа равна true, то ничего плохого не происходит. Если константа равна false, то возникает ошибка компиляции с сообщением, которое задано 2-м аргументом static_assert — строковым литералом.
Проверки различных условий, накладываемых на параметры шаблонов, обычно производятся при помощи других шаблонов — определенных в файле <type_traits>. Например, std::is_arithmetic<T> — это шаблонная структура, у которой есть поле static constexpr bool value;, которое имеет значение true, если параметр шаблона T является числовым типом и false — если нечисловым. А как это так, для одних параметров шаблона поле структуры имеет одно значение, а для других — другое? C++ позволяет создавать разный код для разных значений параметра шаблона — это называется специализация шаблона (template specialization). Вот как гипотетически могла бы выглядеть структура std::is_arithmetic в сильно упрощенном варианте:
template<typename T>
struct is_arithmetic
{
static constexpr bool value = false; // FALSE by default
};
// ... except for the following types:
// the following is a template specialization
template<>
struct is_arithmetic<int>
{
static constexpr bool value = true; // TRUE for T=int
}
// the following is a template specialization
template<>
struct is_arithmetic<float>
{
static constexpr bool value = true; // TRUE for T=float
}
// et cetera...
Что осталось за кадром
Есть ряд вещей, о которых мне уже лень писать, но я их хотя бы перечислю:
- constexpr — ключевое слово, которое позволяет создавать функции, которые вычисляются во время компиляции, что позволяет не тратить время на эти вычисления во время выполнения программы.
- noexcept — ключевое слово, которым маркируются функции. Оно сообщает компилятору о том, что данная функция не генерирует исключений (по крайней мере, программист предполагает, то это так). Это знание позволяет компилятору сгенерировать более эффективный код при вызове функции.
- decltype, auto, decltype(auto) — все три варианта можно использовать для выведения типа возвращаемого значения функции, правда могут получиться разные результаты — об этом написано в [Meyers, Item 3. Understand decltype]