Поправить правила преобразования типов
ssoft-hub opened this issue · 2 comments
В языке существует особенность, которая проявляется при реализации паттерна Adapter (Proxy, Wrapper).
Простейшая реализация Proxy, который агрегирует значение и при преобразовании к типу вложенного значения сохраняет свойства rvalue/lvalue и контантность, выглядит следующим образом:
template < typename T >
class Proxy
{
private:
T m_v;
public:
T && get () && { return static_cast< T && >(m_v); }
T const && get () const && { return static_cast< T const && >(m_v); }
T & get () & { return m_v; }
T const & get () const & { return m_v; }
};
Случай для спецификатора volitile добавляет еще 4 варианта методов, но для упрощения здесь не рассматриваются.
Явное преобразование типа Proxy к типу вложенного значения с помощью метода get() позволяет бесшовно использовать экземпляры Proxy. Следующий код позволяет в этом убедиться:
class Data {};
using ProxyData = Proxy< Data >;
ProxyData foo () { return {}; }
ProxyData const cfoo () { return {}; }
void bar ( MyData && other ) {}
void bar ( MyData const && other ) {}
void bar ( MyData & other ) {}
void bar ( MyData const & other ) {}
int main ()
{
// rvalue / mutable
{
Data data = foo().get();
data = foo().get();
bar( foo().get() );
}
// rvalue / const
{
Data data = cfoo().get();
data = cfoo().get();
bar( cfoo().get() );
}
// lvalue / mutable
{
ProxyData proxy;
Data data = proxy.get();
data = proxy.get();
bar( proxy.get() );
}
// lvalue / const
{
ProxyData const proxy;
Data data = proxy.get();
data = proxy.get();
bar( proxy.get() );
}
}
Не всегда удобно использовать метод get(), особенно при множественном вложении значения в разные Proxy. Хотелось бы использовать экземпляр Proxy в выражениях наравне с экземплярами вложенного типа "прозрачно", без явного приведения с помощью метода get().
Если попытаться заменить явный метод get() на пользовательский оператор преобразования типа, то это приведет к ошибкам компиляции для временного экземпляра Proxy (компилятор gcc7 данный код компилирует без ошибок).
template < typename T >
class Proxy
{
private:
T m_v;
public:
operator T && () && { return static_cast< T && >(m_v); }
operator T const && () const && { return static_cast< T const && >(m_v); }
operator T & () & { return m_v; }
operator T const & () const & { return m_v; }
};
class Data {};
using ProxyData = Proxy< Data >;
ProxyData foo () { return {}; }
ProxyData const cfoo () { return {}; }
void bar ( MyData && other ) {}
void bar ( MyData const && other ) {}
void bar ( MyData & other ) {}
void bar ( MyData const & other ) {}
int main ()
{
// rvalue / mutable
{
Data data = foo();
data = foo(); // error: ambiguous overload for 'operator='
bar( foo() ); // error: call of overloaded 'bar(ProxyData)' is ambiguous
}
// rvalue / const
{
Data data = cfoo();
data = cfoo(); // error: ambiguous overload for 'operator='
bar( cfoo() ); // error: call of overloaded 'bar(ProxyData)' is ambiguous
}
// lvalue / mutable
{
ProxyData proxy;
Data data = proxy;
data = proxy;
bar( proxy );
}
// lvalue / const
{
ProxyData const proxy;
Data data = proxy;
data = proxy;
bar( proxy );
}
}
Такая ситуация связана с тем, что результат функции foo() одинаково хорошо стандартно преобразуется в ссылку rvalue и константную ссылку lvalue на временный экземпляр Proxy, который в свою очередь может быть одинаково пользовательски преобразован в ссылку rvalue и константную ссылку lvalue на внутреннее значение. Цепочки преобразований равнозначные - компилятор не может выбрать одну из них.
Если стандартизировать преобразование в ссылку rvalue предпочтительнее преобразования в константную ссылку lvalue, то данную ситуацию можно было бы разрешить однозначно верно.
Такое изменение не привнесет нежелательных побочных эффектов в существующую кодовую базу, так как только уточняет правила связывания временных объектов с сылками (работает в gcc7) и позволит реализовать пользовательские операторы преобразования типов с сохранением свойств rvalue/lvalue и const/volatile.
Ссылка на исходный код godbolt
Нововведения в стандарт c++23 p0847r6 позволяют написать обертку в виде
#include <memory>
template < typename Self, typename Type >
using like_t = decltype( ::std::forward_like< Self >( ::std::declval< Type >() ) );
template < typename T >
struct Proxy
{
T m_v;
template < typename Self >
operator like_t< Self, T > ( this Self && self )
{
return ::std::forward_like< Self >( self.Proxy::m_v );
}
};
struct Data {};
using ProxyData = Proxy< Data >;
void bar ( Data && other ) {}
void bar ( Data const && other ) {}
void bar ( Data & other ) {}
void bar ( Data const & other ) {}
ProxyData foo () { return {}; }
ProxyData const cfoo () { return {}; }
int main ()
{
// rvalue / mutable
{
Data data = foo();
data = foo();
bar( foo() );
}
// rvalue / const
{
Data data = cfoo();
data = cfoo();
bar( cfoo() );
}
// lvalue / mutable
{
ProxyData proxy;
Data data = proxy;
data = proxy;
bar( proxy );
}
// lvalue / const
{
ProxyData const proxy;
Data data = proxy;
data = proxy;
bar( proxy );
}
return 0;
}
Такая реализация без проблем собирается и правильно работает. На мой взгляд это ещё один повод, чтобы явная реализация операторов преобразования работала подобным образом.
PS: Так же хорошо бы в стандарт добавить тип ::std::like_t наравне с добавленным уже ::std::forward_like.
Ссылка на исходный код godbolt
Скоро C++ будет как Perl только C++. И глядя на код можно будет сразу точно сказать, что вообще не понятно что он делает и почему делает именно так, то что он делает и где прячится UB из за которого иногда происходит не совсем то что было задумано изначально.