В этом посте я расскажу о новой и (как мне кажется) относительно малоизвестной фиче C++ - reference-qualified member functions. Расскажу о правилах перегрузки таких функций, а также, в качестве примера использования, расскажу, как с помощью ref-qualified функций можно попытаться улучшить схему управления ресурсами, реализуемую с помощью другой идиомы С++ — RAII.
Введение
Итак, с недавнего времени в С++ появилась возможность квалифицировать функции-члены ссылкой (по крайней мере, внешне это выглядит как ссылка). Эти знаки квалификации могут быть lvalue, rvalue ссылками, могут сочетаться с const квалификацией.
class some_type
{
void foo() & ;
void foo() && ;
void foo() const & ;
void foo() const && ;
};
Зачем это нужно?
Строго говоря, официально это фича называется немного по-другому, а именно “ref-qualifiers for *this” или “rvalue references for *this”. Но мне кажется это название немного сбивает с толку, так как может показаться, что объект меняет тип при вызове функций с различной квалификацией. А на самом деле, тип *this никогда не меняется. Так в чем же фишка? А фишка в том, что благодаря этим квалификаторам становится возможным перегружать функции-члены по контексту (rvalue, lvalue, etc) в котором используется объект.
int main()
{
some_type t;
t.foo(); // some_type::foo() &
some_type().foo(); // some_type::foo() &&
}
Как это работает?
Начнем с того, что в С++ уже давно существует механизм разрешения перегрузки между функциями-членами и свободными функциями. Зачем он нужен спросите вы, ведь и так можно понять вызывается ли свободная функция или метод класса хотя бы внешне, по синтаксису, в одном случае obj.f(), в другом просто f()? Дело в том, что когда дело доходит до перегрузки операторов, различий в синтаксисе уже может и не быть. Например
struct some_type
{
bool operator == (int);
};
bool operator == (const some_type& l, long r);
void g()
{
some_type t;
int i = 42;
t == i; // Какую функцию вызвать?
}
Для разрешения такой перегрузки компилятор представлял функцию-член в виде свободной функции с дополнительным параметром — ссылкой на объект, у которого происходит вызов функции и дальше разрешал перегрузку среди всех свободных функций. Так что для реализации нововведения нужно было всего лишь немного “подкрутить” уже существующее поведение, а именно создавать различные сигнатуры кандидатов перегрузки для различно квалифицированных функций-членов.
Скажу еще пару слов о том, как конкретно работает этот механизм, ибо далеко не всегда очевидно, какая именно функция является лучшим кандидатом для перегрузки в том или ином случае. Рассмотрим еще раз код из первого примера.
class some_type
{
void foo() & ; // 1
void foo() && ; // 2
void foo() const & ; // 3
void foo() const && ; // 4
};
void g()
{
some_type().foo();
}
Для этого вызова подходят 3 кандидата: 2, 3 и 4. Для разрешения между ними в стандарте существуют особые правила, которые на бумаге выглядят довольно многословными и сложными, но суть которых сводится к тому, что выбирается функция, наиболее точно соответствующая типу.
Попробую пересказать цепь рассуждений по выводу кандидата, как я ее себе представляю. В данном примере выражение some_type() — rvalue. Потенциально могут быть вызваны функции 2, 3 или 4. Но rvalue reference квалифицированные функции более “соответствуют” типу исходного выражения (rvalue), чем const &. Остаются варианты 2 и 4. В четвертом варианте для полного соответствия нужно сделать дополнительное действие над исходным типом — добавить const, тогда как во 2ом варианте никаких дополнительных действий не требуется. Поэтому в итоге будет выбран вариант 2.
Как использовать?
Использовать это нововведение, очевидно, удобно в тех случаях когда поведение объекта должно различаться от контекстов, в котром он используется. Например, мы можем сделать более безопасным использование указателя на хранимый ресурс при использовании RAII.
class file_wrapper
{
public:
// ...
operator FILE* () {return held_;}
~file_wrapper() {fclose(held_);}
private:
FILE* held_;
};
В данном примере operator FILE* () представляет собой огромную дыру в безопасном использовании файловой обертки.
Представьте себе такой контекст использования:
FILE* f = file_wrapper("some_file.txt", "r");
// Работа с f
Теперь у нас появляется возможность сделать эту, в сущности очень удобную, функцию более (но не полностью) безопасной.
operator FILE* () // Можно вызвать только у lvalue объектов
Можно посмотреть на RAII и с немного другой стороны. Раз мы можем теперь “понять”, что нас вызывают в разных контекстах, давайте просто передавать владение ресурсом вместо копирования в тех случаях, когда дальше использоваться наш объект больше не будет.
template <typename T>
class some_type
{
public:
operator std::unique_ptr<T>() const operator std::unique_ptr<T>() &private:
std::unique_ptr<T> held_;
};
some_type f();
void g()
{
std::unique_ptr<widget> p = f();
}
Автор: rpz