Pull to refresh

Ref-qualified member functions

C++ *
В этом посте я расскажу о новой и (как мне кажется) относительно малоизвестной фиче 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) const; 
};
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* () & {return held_;} // Можно вызвать только у lvalue объектов


Можно посмотреть на RAII и с немного другой стороны. Раз мы можем теперь “понять”, что нас вызывают в разных контекстах, давайте просто передавать владение ресурсом вместо копирования в тех случаях, когда дальше использоваться наш объект больше не будет.

template <typename T>
class some_type
{ 
public:
  operator std::unique_ptr<T>() const &
  {
    return std::unique_ptr<T>(new T(*held_)); // Копируем
  } 
  operator std::unique_ptr<T>() &&
  { 
    return std::move(held_); // Отдаем владение
  } 
private:
  std::unique_ptr<T> held_;
};

some_type f();

void g()
{
  std::unique_ptr<widget> p = f();
}
Tags:
Hubs:
Total votes 53: ↑49 and ↓4 +45
Views 18K
Comments 24
Comments Comments 24