Билеты 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. Ссылки на какие объекты уместно возвращать из функции?

Не стоит возвращать из функции ссылки на локальные объекты, иначе это приведет к висячим ссылкам. В каких случаях вернуть ссылку из функции уместно:

  1. Если функция позволяет получить доступ к элементу какого-то контейнера, тогда передача по ссылке — это альтернатива методам get, set (в случаях, когда вы не хотите, чтобы кто-то мог менять объект, либо возвращайте по значению, либо по константной ссылке (первое безопасно, второе может вам навредить с помощью const_cast, так что все же лучше по значению)).
  2. Если вы хотите делать что-то типа x.f().f().g().size(), и при этом все манипуляции происходили над одним и тем же объектом (пример operator=).
  3. *Наверняка есть что-то еще типа Синглтона или что-то с использованием динамической памяти (зачем если есть указатели), можно дополнить, но я думаю этого достаточно.

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). Дальше приведены примеры как это влияет на указатель базового класса от производного и зачем нужны виртуальные функции (в билете не спрашивается, для общего развития).