cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

Поправить правила преобразования типов #552

Open ssoft-hub opened 1 year ago

ssoft-hub commented 1 year ago

В языке существует особенность, которая проявляется при реализации паттерна 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

ssoft-hub commented 1 year ago

Нововведения в стандарт 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

kov-serg commented 1 year ago

Скоро C++ будет как Perl только C++. И глядя на код можно будет сразу точно сказать, что вообще не понятно что он делает и почему делает именно так, то что он делает и где прячится UB из за которого иногда происходит не совсем то что было задумано изначально.