С врапперами (wrapper, обёртка) так или иначе сталкиваются все программисты. В этой статье под враппером я понимаю класс, который оборачивает экземпляр какого-то другого типа. Делаться это может по разным причинам (реализация другого интерфейса, RAII и др.).
Я бы хотел остановиться на том, как новые возможности языка C++ влияют на подходы к написанию врапперов. Мы посмотрим на такие «новые» возможности, как семантика перемещения, constexpr if, ref-qualifiers и deducing this.
Простейший враппер до C++11
Ниже приводится пример реализации и использования простейшего враппера до C++11.
#include <type_traits>
template<typename T>
class Wrapper
{
public:
explicit Wrapper(const T &v) : m_value(v)
{}
T& get() { return m_value; }
const T& get() const { return m_value; }
private:
T m_value;
};
//--------------------------------------------
template <typename T>
T& get_value(Wrapper<T> &w)
{
return w.get();
}
template <typename T>
const T& get_value(const Wrapper<T> &w)
{
return w.get();
}
//--------------------------------------------
int main()
{
Wrapper<int> w(42);
auto&& v = w.get();
static_assert(std::is_same_v<decltype(v), int&>);
const Wrapper<int> cw(42);
auto&& cv = cw.get();
static_assert(std::is_same_v<decltype(cv), const int&>);
auto&& v2 = get_value(w);
static_assert(std::is_same_v<decltype(v2), int&>);
auto&& cv2 = get_value(cw);
static_assert(std::is_same_v<decltype(cv2), const int&>);
return 0;
}
Ограничения стандарта я накладываю только на реализацию враппера и функции get_value. Другие инструменты, появившиеся после C++11 и используемые в функции main (auto, decltype и др.), используются только для демонстрации работы враппера.
Главное, что нас здесь интересует — это типы получаемых значений: v, cv, v2 и cv2. Все они получают тот тип, который мы ожидаем: константная или неконстантная ссылка на значение внутри враппера. Правда для этого нам пришлось написать две реализации функции get_value.
Если метод get вызывается для константного экземпляра класса Wrapper, мы получаем константную ссылку. Если же он вызывается для неконстантного экземпляра — неконстантную. Эта же логика реализуется и в функции get_value. Если в неё передается константная ссылка на экземпляр враппера, мы получаем константную ссылку на значение внутри враппера. Если же в функцию передается неконстантная ссылка на враппер, мы получаем неконстантную ссылку на значение внутри враппера. Всё логично и прекрасно.
Семантика перемещения
Одним из важнейших нововведений C++11 стала семантика перемещения (move semantics) [1]. У нас появился еще один вид ссылок.
Применительно к нашему врапперу это значит, что теперь мы хотим новую функциональность. Мы хотим, чтобы метод get и функция get_value могли возвращать r-value ссылку на значение внутри враппера. Как этого можно добиться?
Ниже приводится возможная реализация.
#include <type_traits>
#include <utility>
template <typename T>
class Wrapper
{
public:
template <typename... Args>
explicit Wrapper(Args&&... args)
: m_value(std::forward<Args>(args)...)
{}
T& get() { return m_value; }
const T& get() const { return m_value; }
private:
T m_value;
};
//--------------------------------------------
namespace impl {
template <bool IsRValue>
struct UnwrapReference;
template <>
struct UnwrapReference<true>
{
template <typename T>
static decltype(auto) get(T &&w)
{
return std::move(w.get());
}
};
template <>
struct UnwrapReference<false>
{
template <typename T>
static decltype(auto) get(T &&w)
{
return w.get();
}
};
} //impl namespace
//--------------------------------------------
template <typename T>
decltype(auto) get_value(T &&w)
{
return impl::UnwrapReference
<std::is_rvalue_reference<decltype(w)>::value>
::get(std::forward<T>(w));
}
//--------------------------------------------
int main()
{
Wrapper<int> w(42);
auto&& v = w.get();
static_assert(std::is_same_v<decltype(v), int&>);
const Wrapper<int> cw(42);
auto&& cv = cw.get();
static_assert(std::is_same_v<decltype(cv), const int&>);
auto&& mv = std::move(w).get();
static_assert(std::is_same_v<decltype(mv), int&>);
auto&& mv2 = std::move(w.get());
static_assert(std::is_same_v<decltype(mv2), int&&>);
auto&& v2 = get_value(w);
static_assert(std::is_same_v<decltype(v2), int&>);
auto&& cv2 = get_value(cw);
static_assert(std::is_same_v<decltype(cv2), const int&>);
auto &&mv3 = get_value(std::move(w));
static_assert(std::is_same_v<decltype(mv3), int&&>);
return 0;
}
Вполне вероятно, у Вас возник вопрос: почему так сложно? Зачем нужна структура UnwrapReference? Чтобы ответить на этот вопрос нужно пристальнее взглянуть на два случая из примера выше:
auto&& mv = std::move(w).get(); static_assert(std::is_same_v<decltype(mv), int&>); auto&& mv2 = std::move(w.get()); static_assert(std::is_same_v<decltype(mv2), int&&>);
В случае mv std::move [2] применяется только к врапперу, и на выходе мы получаем l-value ссылку. А в случае mv2 std::move применяется уже к результату выполнения метода get, и мы получаем ожидаемую r-value ссылку. Такое небольшое различие приводит к совершенно разным результатам.
В текущей реализации у класса Wrapper нет подходящего метода для получения r-value ссылки на значение внутри него. Поэтому приходится явно конвертировать возвращенную l-value ссылку в r-value ссылку (случай mv2).
В случае функции get_value хотелось бы написать что-нибудь вроде этого:
template <typename T>
decltype(auto) get_value(T &&w)
{
if(std::is_rvalue_reference<decltype(w)>::value)
return std::move(w.get());
else
return w.get();
}
К сожалению, этот код не скомпилируется. Условие if проверяется во время работы программы (runtime). Поэтому на момент компиляции компилятор не может определить значение какого типа будет возвращать функция (int& или int&&). Текст ошибки выглядит так (компилятор GCC):
main.cpp:68:22: error: inconsistent deduction for auto return type: ‘int&&’ and then ‘int&’ 68 | return w.get();
Вы наверняка вспомнили о constexpr if. Он появился в C++17, и мы поговорим о нём чуть позже. Как можно решить нашу проблему средствами C++11?
Для этого нам и нужна вспомогательная структура UnwrapReference. Поскольку она является вспомогательной, мы располагаем её в пространстве имён impl (иногда для этих же целей используется имя details).
Структура UnwrapReference имеет 1 нетиповой шаблонный параметр типа bool. Если он равен true, то статический метод get этой структуры должен возвращать r-value ссылку. Если же шаблонный параметр равен false — l-value ссылку. Требуемое поведение метода задаётся путём определения двух полных специализаций структуры UnwrapReference.
Статический метод UnwrapReference::get также является шаблонным. Таким образом он не зависит от конкретного типа враппера.
С учетом вышесказанного функция get_value имеет вид:
template <typename T>
decltype(auto) get_value(T &&w)
{
return impl::UnwrapReference
<std::is_rvalue_reference<decltype(w)>::value>
::get(std::forward<T>(w));
}
Единственное, что в ней делается, это вызов статического метода get класса UnwrapReference. Требуемый тип возвращаемого значения (l-value или r-value) определяется на основе типа входного параметра w, для анализа которого используется метафункция std::is_rvalue_reference [3].
constexpr if
constexpr if statement — это возможность, появившаяся в C++17 [4]. Она позволяет объявлять конструкции if, которые будут выполнены во время компиляции (compile time). И это именно то, что нам нужно для реализации функции get_value в том виде, в котором мы бы хотели её видеть.
Ниже приводится доработанная реализация нашего враппера с использованием constexpr if.
#include <type_traits>
#include <utility>
template <typename T>
class Wrapper
{
public:
template <typename... Args>
explicit Wrapper(Args&&... args)
: m_value(std::forward<Args>(args)...)
{}
T& get() { return m_value; }
const T& get() const { return m_value; }
private:
T m_value;
};
//--------------------------------------------
template <typename T>
decltype(auto) get_value(T &&w)
{
if constexpr (std::is_rvalue_reference_v<decltype(w)>)
return std::move(w.get());
else
return w.get();
}
//--------------------------------------------
int main()
{
Wrapper<int> w(42);
auto&& v = w.get();
static_assert(std::is_same_v<decltype(v), int&>);
const Wrapper<int> cw(42);
auto&& cv = cw.get();
static_assert(std::is_same_v<decltype(cv), const int&>);
auto&& mv = std::move(w).get();
static_assert(std::is_same_v<decltype(mv), int&>);
auto&& mv2 = std::move(w.get());
static_assert(std::is_same_v<decltype(mv2), int&&>);
auto&& v2 = get_value(w);
static_assert(std::is_same_v<decltype(v2), int&>);
auto&& cv2 = get_value(cw);
static_assert(std::is_same_v<decltype(cv2), const int&>);
auto &&mv3 = get_value(std::move(w));
static_assert(std::is_same_v<decltype(mv3), int&&>);
return 0;
}
Обратите внимание, благодаря constexpr if нам больше не нужна вспомогательная структура UnwrapReference. А использование std::is_rvalue_reference_v вместо std::is_rvalue_reference позволило еще немного уменьшить размер кода.
При работе с constexpr if нужно помнить о том, что все его ветви должны быть синтаксически корректны, даже если они никогда не будут вызваны. Так, пример ниже вызовет ошибку компиляции.
int main()
{
if constexpr (false)
{
fake_name_causes_error;
}
return 0;
}
Этим constexpr if принципиально отличается от директив препроцессора (#ifdef и др.)
Хорошо. Вернёмся к нашему врапперу. Можно ли еще как-нибудь упростить его код?
На самом деле можно. И тут я должен покаяться перед Вами, так как умышленно повёл Вас по неправильному пути. Я сделал это, чтобы более наглядно продемонстрировать Вам всю мощь той конструкции, о которой многие забывают. Несмотря на то, что появилась она еще в C++11.
Но прежде чем перейти к ней, давайте еще раз обозначим ту проблему, которая мешает нам упростить враппер.
Суть проблемы
Давайте еще раз посмотрим на несколько случаев из нашего примера:
Wrapper<int> w(42); auto&& v = w.get(); static_assert(std::is_same_v<decltype(v), int&>); const Wrapper<int> cw(42); auto&& cv = cw.get(); static_assert(std::is_same_v<decltype(cv), const int&>); auto&& mv = std::move(w).get(); static_assert(std::is_same_v<decltype(mv), int&>);
Переменная w представляет собой l-value. Когда мы вызываем для неё метод get(), мы получаем l-value ссылку.
Переменная cw представляет собой константную l-value. Когда мы вызываем для неё метод get(), мы получаем константную l-value ссылку. Пока всё сходится.
В строке 9 метод get() вызывается для r-value, но возвращает он l-value ссылку. Почему? Давайте посмотрим на методы get().
T& get() { return m_value; }
const T& get() const { return m_value; }
Хорошо. У нас нет метода, который возвращал бы r-value ссылку. Как его создать? Можно написать что-нибудь вроде такого:
T&& get_rvalue() { return std::move(m_value); }
При таком подходе мы должны будем сами определять, когда какой метод вызывать, или обмазываться вспомогательными структурами по типу UnwrapReference. Нет, нужно что-то другое.
Давайте вспомним, что методы класса — это, по сути, те же функции, но с одним дополнительным параметром — указателем (для простоты считаем ссылкой) на экземпляр класса, для которого вызывается метод. Таким образом, приведенные выше методы get, по сути, являются перегрузками вида (для простоты я опускаю, что wrapper — шаблонный класс).
// T& wrapper::get() { return m_value; }
T& wrapper_get(wrapper &r_this)
{ return r_this.m_value; }
// const T& wrapper::get() const { return m_value; }
const T& wrapper_get(const wrapper &r_this)
{ return r_this.m_value; }
Обратите внимание, все эти перегрузки осуществляются по указателю (ссылке) на экземпляр класса (указатель this). Получается, нам нужна еще одна перегрузка вида:
T&& wrapper_get(wrapper &&r_this)
{
return std::move(r_this.m_value);
}
Но, как её задать? Для этого и используются ref-qualifiers.
ref-qualifiers
ref-qualifiers (member functions with ref-qualifier, ref-qualification) — это возможность, которая позволяет явно задавать ссылочный квалификатор (reference qualifier) для параметра this нестатического метода класса [5,6].
Ниже приводится доработанная реализация нашего враппера с использованием ref-qualifiers.
#include <type_traits>
#include <utility>
template <typename T>
class Wrapper
{
public:
template <typename... Args>
explicit Wrapper(Args&&... args)
: m_value(std::forward<Args>(args)...)
{}
T& get() & { return m_value; }
const T& get() const & { return m_value; }
T&& get() && { return std::move(m_value); }
const T&& get() const && { return std::move(m_value); }
private:
T m_value;
};
//--------------------------------------------
template <typename T>
decltype(auto) get_value(T &&w)
{
return std::forward<T>(w).get();
}
//--------------------------------------------
int main()
{
Wrapper<int> w(42);
auto&& v = w.get();
static_assert(std::is_same_v<decltype(v), int&>);
const Wrapper<int> cw(42);
auto&& cv = cw.get();
static_assert(std::is_same_v<decltype(cv), const int&>);
auto&& mv = std::move(w).get();
static_assert(std::is_same_v<decltype(mv), int&&>);
auto&& mv2 = std::move(w.get());
static_assert(std::is_same_v<decltype(mv2), int&&>);
auto&& v2 = get_value(w);
static_assert(std::is_same_v<decltype(v2), int&>);
auto&& cv2 = get_value(cw);
static_assert(std::is_same_v<decltype(cv2), const int&>);
auto &&mv3 = get_value(std::move(w));
static_assert(std::is_same_v<decltype(mv3), int&&>);
return 0;
}
Обратите внимание на два момента. Во-первых, переменная mv теперь r-value ссылка на значение внутри враппера. Как мы этого и хотели. Во-вторых, функция get_value сократилась всего до одной строчки. Она стала даже проще, чем при использовании constexpr if. Всю необходимую работу делает функция std::forward [7] и ref-qualifiers.
ref-qualifiers, нюансы
При работе с ref-qualifiers нужно помнить о двух важных нюансах.
Во-первых, на странице [8] говорится:
unlike cv-qualification, ref-qualification does not change the properties of the
thispointer: within an rvalue ref-qualified function,*thisremains an lvalue expression.
Или в моём переводе на русский:
В отличие от cv-qualification, ref-qualification не меняет свойства указателя
this: внутри функции, помеченной r-value через ref-qualification, выражение*thisостаётся l-value выражением.
Во-вторых, ref-qualification нельзя смешивать с cv-qualification. Так, пример ниже вызовет ошибку компиляции:
#include <utility>
template <typename T>
class Wrapper
{
public:
template <typename... Args>
explicit Wrapper(Args&&... args)
: m_value(std::forward<Args>(args)...)
{}
//ref-qualification
T& get() & { return m_value; }
//cv-qualification
const T& get() const { return m_value; }
private:
T m_value;
};
//--------------------------------------------
int main()
{
return 0;
}
В компиляторе GCC текст ошибки будет выглядеть примерно так:
<source>:17:14: error: 'const T& Wrapper<T>::get() const' cannot be overloaded with 'T& Wrapper<T>::get() &' [-Wtemplate-body]
17 | const T& get() const { return m_value; }
| ^~~
<source>:14:8: note: previous declaration 'T& Wrapper<T>::get() &'
14 | T& get() & { return m_value; }
| ^~~
deducing this
Единственный серьёзный недостаток ref-qualifiers — это необходимость написания 4 различных перегрузок для одной и той же функции. Для решения этой проблемы в C++23 добавили deducing this [9]. Это специальный синтаксис, который позволяет нам задавать конкретный тип (набор квалификаторов) для указателя this.
Ниже приводится доработанная реализация нашего враппера с использованием deducing this.
#include <type_traits>
#include <utility>
template <typename T>
class Wrapper
{
public:
template <typename... Args>
explicit Wrapper(Args&&... args)
: m_value(std::forward<Args>(args)...)
{}
template <typename Self>
auto&& get(this Self &&self)
{
return std::forward<Self>(self).m_value;
}
private:
T m_value;
};
//--------------------------------------------
template <typename T>
decltype(auto) get_value(T &&w)
{
return std::forward<T>(w).get();
}
//--------------------------------------------
int main()
{
Wrapper<int> w(42);
auto&& v = w.get();
static_assert(std::is_same_v<decltype(v), int&>);
const Wrapper<int> cw(42);
auto&& cv = cw.get();
static_assert(std::is_same_v<decltype(cv), const int&>);
auto&& mv = std::move(w).get();
static_assert(std::is_same_v<decltype(mv), int&&>);
auto&& mv2 = std::move(w.get());
static_assert(std::is_same_v<decltype(mv2), int&&>);
auto&& v2 = get_value(w);
static_assert(std::is_same_v<decltype(v2), int&>);
auto&& cv2 = get_value(cw);
static_assert(std::is_same_v<decltype(cv2), const int&>);
auto &&mv3 = get_value(std::move(w));
static_assert(std::is_same_v<decltype(mv3), int&&>);
return 0;
}
Обратите внимание, метод get не имеет перегрузок. Теперь он является шаблонным и конкретизируется (инстанцируется) по типу переданного в него указателя this.
Другим важным моментом является то, что тип возвращаемого методом get значения задаётся посредством auto&&. Если задавать его посредством decltype(auto), то получается иной результат. Практически все переменные (v, cv, v2 и др.) становятся r-value ссылками. А для переменной mv2 выдаётся предупреждение:
<source>: In function 'int main()':
<source>:45:12: warning: possibly dangling reference to a temporary [-Wdangling-reference]
45 | auto&& mv2 = std::move(w.get());
| ^~~
<source>:45:33: note: 'int' temporary created here
45 | auto&& mv2 = std::move(w.get());
| ~~~~~^~
Почему это происходит, я пока не понимаю. Скорее всего это связано с точным типом возвращаемого методами get() значения. Нужно будет более детально посмотреть этот вопрос. Если у Вас есть мысли (идеи) на этот счет — пишите.
Заключение
В этой статье через призму простейшего (и бесполезного) класса-враппера мы рассмотрели такие возможности языка С++, как constexpr if, ref-qualifiers и deducing this. Мы увидели, как каждая из них решает проблемы, возникающие при написании врапперов.
Конечно, они используются не только при написании врапперов. Но именно во врапперах раскрывается их основная сила.
Ссылки
- Вся 11 глава этого руководства посвящена семантике перемещения: https://metanit.com/cpp/tutorial/14.1.php
- Функция
std::move: https://en.cppreference.com/w/cpp/utility/move.html - Метафункция
std::is_rvalue_reference: https://en.cppreference.com/w/cpp/types/is_rvalue_reference.html constexpr if statement: https://en.cppreference.com/w/cpp/language/if.html#Constexpr_if- Статья
Александра Куликова«Ref-qualified member functions«: https://habr.com/ru/articles/216783/ - Статья
Andrzej Krzemieński«Ref-qualifiers«: https://akrzemi1.wordpress.com/2014/06/02/ref-qualifiers/ - Функция
std::forward: https://en.cppreference.com/w/cpp/utility/forward.html ref-qualifiers: https://en.cppreference.com/w/cpp/language/member_functions.html#Member_functions_with_ref-qualifier- Статья
Sy Brand«C++23’s Deducing this: what it is, why it is, how to use it«: https://devblogs.microsoft.com/cppblog/cpp23-deducing-this/
