Билеты 65-74
65. Чем отличаются методы std::vector
push_back
и emplace_back
?
Краткий ответ
emplace_back
- более новая версияpush_back
, она способна эффективнее работать с rvalue значениями и допускает передачу аргументов для конструктора объекта без явного создания
Пример использования
class T {
public:
T(int, int) {
std::cout << "ctor called" << std::endl;
}
T(int) {
std::cout << "ctor called" << std::endl;
}
T(T&&) {
std::cout << "move ctor called" << std::endl;
}
T(const T&) {
std::cout << "copy ctor called" << std::endl;
}
};
void emplace() {
std::cout << "emplace rvalue" << std::endl;
std::vector<T> vector;
vector.emplace_back(1);
}
void push() {
std::cout << "push rvalue" << std::endl;
std::vector<T> vector;
vector.push_back(1);
}
int main() {
push();
emplace();
std::vector<T> v;
v.push_back(1, 2); // Error
v.push_back({1, 2}); // Ok
v.emplace_back(1, 2); // Ok
}
Вывод программы
push rvalue
ctor called
move ctor called
emplace rvalue
ctor called
- Итак, новая версия добавления элемента способна внутри себя создать объект из переданных ей аргументов и добавить в вектор без лишних перемещений. Старая же способна только принять в себя объект типа
T
, а значит будет вызван конструктор временного объекта, а затем перемещающий конструктор для добавления в вектор (более того,push_back
используетemplace_back
, если к нему приходит объект типаT&&
, что позволяет ему не делатьcopy ctor
, а толькоmove ctor
). - К тому же благодаря новой концепции,
emplace_back
может принять в себя несколько аргументов без вызова конструктора, что может быть полезно.
Важно отметить, что различные методы emplace
и push
это методы не только вектора, а и многих других контейнеров и адаптеров.
Вывод
Не используйте версию push
.
``
66. Ссылки на какие объекты уместно возвращать из функции?
Не стоит возвращать из функции ссылки на локальные объекты, иначе это приведет к висячим ссылкам. В каких случаях вернуть ссылку из функции уместно:
- Если функция позволяет получить доступ к элементу какого-то контейнера, тогда передача по ссылке — это альтернатива методам
get
,set
(в случаях, когда вы не хотите, чтобы кто-то мог менять объект, либо возвращайте по значению, либо по константной ссылке (первое безопасно, второе может вам навредить с помощьюconst_cast
, так что все же лучше по значению)). - Если вы хотите делать что-то типа
x.f().f().g().size()
, и при этом все манипуляции происходили над одним и тем же объектом (примерoperator=
). - *Наверняка есть что-то еще типа Синглтона или что-то с использованием динамической памяти (зачем если есть указатели), можно дополнить, но я думаю этого достаточно.
67. Что такое std::move
(вариант, принимающий один аргумент) и зачем она нужна?
Кратки ответ
Глобально, данная функция позволяет вам гарантировать, что дальнейшее использование переменной типа lvalue не будет, и его ресурсы можно использовать для нужд других объектов. В особенности
std::move
возвращает xvalue, ее вызов эквивалентенstatic_cast<T&&>
.
Буквально в type_traits.cpp
написано:
constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
Наиболее понятное и частое использование std::move
состоит в том, чтобы избежать лишнего копирования и сделать относительное дешевое перемещение для объекта, который нам больше не нужен при передаче его в конструктор.
68. Что такое std::forward
и зачем она нужна?
Задачка со звездочкой.
Кратки ответ
Для чего нужен
std::forward
? Чтобы передать аргументы ровно такого value category, какой был у них изначально.
Такая концепция передачи аргументов называется perfect forwarding (ранее это было использовано emplace'ом). Такое поведение позволяет нам избежать лишних копирований и перемещений.
#include <iostream>
#include <utility>
void f(int& x) {
std::cout << "&: " << x << "\n";
}
void f(int&& x) {
std::cout << "&&: " << x << "\n";
}
template<typename T>
void wrapper(T&& arg) {
// !!!Передаем arg дальше с сохранением его типа!!
f(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // lvalue
wrapper(20); // rvalue
}
output:
&: 10
&&: 20
Пример perfect forwarding с помощью std::forward
. Важно отметить конструкцию шаблона функции wrapper
, из-за правил вывода шаблона и схлопывания ссылок аргумент функции arg
является универсальной ссылкой (или forwarding reference), т.е для lvalue arg будет lvalue и для rvalue arg будет rvalue.
P.S что std::move
, что std::forward
можно заменить (и фактически они заменяются) на static_cast<T&/T&&>
, но как и для любого велосипеда, это может привести к ошибкам, и встроенные функции позволяют нам обработать например std::move(std::forward<int&>(7));
. К тому же такие функции гораздо легче читать и понимать, что намного важнее чем сэкономить на вызове функции.
69. Какие конструкторы (и сколько раз) класса T
будут вызваны:
T f(int n) { return n; }
auto x = f(101);
Кратки ответ
Только конструктор Т
class T {
public:
T(int) {
std::cout << "ctor called" << std::endl;
}
T(T&&) {
std::cout << "move ctor called" << std::endl;
}
T(const T&) {
std::cout << "copy ctor called" << std::endl;
}
};
T f(int n) {
return n;
}
int main() {
auto x = f(101);
}
output:
ctor called
В данном случае кажется, что при возврате функции f будет создан объект типа T, а затем вызовется перемещающий конструктор типа T для переменной x от возвращенного значения функции f. Однако вывод программы другой и все благодаря RVO (return value optimization), которая благодаря компилятору не стала создавать временный объект при возврате из функции, а создала его прямо для х.
70. Какие конструкторы (и сколько раз) класса T
будут вызваны:
T f(T x) { return x; }
auto y = f(101);
Кратки ответ
Конструктор Т и перемещающий контруктор Т
(Код для этого билета смотри выше и поменяй Int на T)
output:
ctor called
move ctor called
Что же поменялось в данном случае? Давайте разберем: мы создаем rvalue объект типа T при передаче функции f значения типа int, соответственно получаем ctor called. Далее RVO и мы создаем х уже не из int, а из T&&, который был сконструирован при вызове — вот и вся разница.
71. Что произойдёт в
T x;
x = T();
Кратки ответ
Два раза вызовется конструктор по умолчанию и перемещающее присваивание.
struct T {
T() {
std::cout << "default" << std::endl;
}
T& operator=(T&&) {
std::cout << "moved assignment" << std::endl;
return *this;
}
};
int main() {
T x;
x = T();
}
output:
default
default
moved assignment
Что произошло: T t
- вызов пустого конструктора класса T (Note: в С++ огромное множество способов инициировать переменную, и это не объявление переменной, а именно ее инициализация и определение с помощью умолчательного конструктора от 0 аргументов). Х = Т() - вызов пустого конструктора класса Т и создание rvalue значения T, затем вызов operator= класса Т от T&&.
72. Корректно ли выражение
f(x.get_value(), std::move(x));
Кратки ответ
Нет, это UB
После того как был использован std::move(x)
мы утверждаем, что в дальнейшем х мы использовать не будем; соответственно его можно переместить. Соответственно если мы будем использовать х после std::move
, то у нас UB. Исходя из этого нам важно чтобы x.get_value()
вызвался раньше чем std::move(x)
, но стандарт C++ не гарантирует порядок исполнения аргументов при передаче функции (и на практике действительно разные компиляторы в разном порядке вызывают аргументы). Соответственно данное выражение не является корректным.
73. Какие операции над объектами типа T
будут вызваны в коде и сколько раз?
void f(std::vector<T> & v) { v.resize(100); }
Кратки ответ
В данном коде может быть вызвано только 2 операции над объектами типа Т: конструктор по умолчанию и деструктор.
Давайте разберемся сколько и чего будет вызвано в зависимости от чего: пусть в функцию f передается вектор размером start, и делается resize размером resize.
static int n{ 0 };
static int m{ 0 };
struct T {
T() {
n++;
std::cout<< "constructor called " << n << " times" << std::endl;
}
~T() {
m++;
std::cout << "destructor called " << m << " times" << std::endl;
}
};
void f(std::vector<T>& vector) {
int resize = 10;
vector.resize(resize);
}
int main() {
int start = 15;
std::vector<T> v(start);
std::cout << "f start" << std::endl;
f(v);
std::cout << "f end" << std::endl;
}
Если размер start < resize, то очевидно конструктор по умолчанию вызовется resize - size раз; если start > resize то для сохранения размера вектора вызовется деструктор resize - size раз; если size = resize то не вызовется ничего.
74. Что будет в если класс Y уже содержит метод
class X : public Y { public: void f(); }
void f();
Кратки ответ
Вызовется функция X::f.
class Y {
public:
void f() {
std::cout << "f: Y" << std::endl;
}
void virtual f_v() {
std::cout << "f_v: Y" << std::endl;
}
};
class X : public Y {
public:
void f() {
std::cout << "f: X" << std::endl;
}
void f_v() override {
std::cout << "f_v: X" << std::endl;
}
};
int main() {
X x;
Y y;
x.f();
y.f();
x.f_v();
y.f_v();
Y* y_ptr = &x;
y_ptr->f();
y_ptr->f_v();
}
output:
f: X
f: Y
f_v: X
f_v: Y
f: Y
f_v: X
В данном случае класс Х наследуется от Y; при вызове x.f() класс X скроет метод Y и вызовет X::f. Если же функции в базовом классе не будет переопределения в наследуемом классе, то будет вызвана функция базового (пример функция g). Дальше приведены примеры как это влияет на указатель базового класса от производного и зачем нужны виртуальные функции (в билете не спрашивается, для общего развития).