Pull to refresh

Иммутабельные данные в C++

Reading time 16 min
Views 16K

Привет, Хабр! Об иммутабельных данных немало говориться, но о реализации на С++ найти что-то сложно. И, потому, решил данный восполнить пробел в дебютной статье. Тем более, что в языке D есть, а в С++ – нет. Будет много кода и много букв.


О стиле – служебные классы и метафункции используют имена в стиле STL и boost, пользовательские классы в стиле Qt, с которой я в основном и работаю.


Введение


Что из себя представляют иммутабельные данные? Иммутабельные данные – это наш старый знакомый const, только более строгий. В идеале иммутабельность означает контекстно-независиую неизменяемость ни при каких условиях.


По сути иммутабельные данные должны:


  • обеспечивать физическую и логическую константность;
  • запрещать присваивание нового значения на этапе компиляции;
    все операции должны проводиться над копией, а не над оригиналом.

Иммутабельные данные пришли из функционального программирования и нашли место в параллельном програмировании, т. к. гарантируют отсутсвие побочных эффектов.


Как можно реализовать иммутабельные данные в С++?
В С++ у нас есть (сильно упрощенно):


  • значения – объекты фундаментальных типов, экземпляры классов (структур, объединений), перечислений;
  • указатели;
    ссылки;
    массивы.

Функции и void не имеет смысл делать иммутабельными. Ссылки тоже не будем делать иммутабельными, для этого есть const reference_wrapper.



Что касается остальных вышеперечисленных типов, то для них можно сделать обертки (а точнее нестандартный защитный заместитель). Что будет в итоге? Цель сделать как-бы модификатор типа, сохранив естественную семантику для работы с объектами данного типа.


Immutable<int> a(1), b(2);

qDebug() << (a + b).value()
         << (a + 1).value()
         << (1 + a).value();

int x[] = { 1, 2, 3, 4, 5 };
Immutable<decltype(x)> arr(x);
qDebug() << arr[0]

Интерфейс


Общий интерфейс прост – всю работу выполняет базовый класс, который выводится из характеристик (traits):


template <typename Type>
class Immutable : public immutable::immutable_impl<Type>::type {
public:
    static_assert(!std::is_same<Type, std::nullptr_t>::value,
                  "nullptr_t cannot used for immutable");
    static_assert(!std::is_volatile<Type>::value,
                  "volatile data cannot used for immutable");
    using ImplType = typename immutable::immutable_impl<Type>;
    using BaseClass = typename ImplType::type;

    using BaseClass::BaseClass;
    using value_type = typename ImplType::value_type;

    constexpr
    Immutable& operator=(const Immutable &) = delete;
};

Запрещая оператор присваивания, мы запрещаем перемещающий оператор присваивания, но не запрещаем перемещающий конструктор.


immutable_impl что-то вроде switch, но по типам (не стал делать такой – слишком усложняет код, да и в простом случае он не особо нужен – ИМХО).


namespace immutable {
    template <typename SrcType>
    struct immutable_impl {
        using Type = std::remove_reference_t<SrcType>;
        using type = std::conditional_t<
            std::is_array<Type>::value,
                array<Type>,
                std::conditional_t <
            std::is_pointer<Type>::value,
                pointer<Type>,
                std::conditional_t <
            is_smart_pointer<Type>::value,
                smart_pointer<Type>,
                immutable_value<Type>
            >
            >
            >;
        using value_type = typename type::value_type;
    };
}

В качестве ограничений явно запретив все операции присваивания (макросы помогают):


template <typename Type, typename RhsType>
constexpr
Immutable<Type>& operator Op=(Immutable<Type> &&, RhsType &&) = delete;

А теперь давайте рассотрим как реализованы отдельные компоненты.


Иммутабельные значения


Под значениями (далее value) понимаются объекты фундаментальных типов, экземпляры классов (структур, объединений), перечислений. Для value у на есть класс, который определяет является ли тип классом, структурой или объединением:


template <typename Type, bool = std::is_class<Type>::value || std::is_union<Type>::value>
class immutable_value;

Если да, то для реализации используется используется CRTP:


template <typename Base>
class immutable_value<Base, true> : private Base
{
public:
    using value_type = Base;
    constexpr
    explicit
    immutable_value(const Base &value)
        : Base(value)
        , m_value(value)
    {
    }
    constexpr
    explicit operator Base() const
    {
        return value();
    }
    constexpr
    Base operator()() const
    {
        return value();
    }
    constexpr
    Base value() const
    {
        return m_value;
    }
private:
    const Base m_value;
};

К сожалению, в С++ пока нет перегрузки оператора .. Хотя, это ожидается в С++ 17 (http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0252r0.pdf, http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0252r0.pdf, http://www.open-std.org/JTC1/SC22/wg21/docs/papers/2015/p0060r0.html), но вопрос еще открыт, ибо коммитет нашел нестыковки.
Тогда бы можно было просто написать:


    constexpr
    Base operator.() const
    {
        return value();
    }

Но решение по этому вопросу ожидается в марте, поэтому для этих целей пока используем оператор ():


    constexpr
    Base operator()() const
    {
        return value();
    }

Обратите внимание, на конструктор:~~


    constexpr
    explicit
    immutable_value(const Base &value)
        : Base(value)
        , m_value(value)
    {
    }

там инициализируется как immutable_value, так и базовый класс. Это позволяет осмысленно манипулировать с immutable_value через operator (). Например:


QPoint point(100, 500);
Immutable<QPoint> test(point);

test().setX(1000); // не поменяет исходный объект
qDebug() << test().isNull() << test().x() << test().y();

Если же тип является встроенным, то реализация будет один-в-один, за исключением базового класса (можно было бы изъвернуться, чтобы соответствовать DRY, но как-то не хотелось усложнять, тем более, что immutable_value делался после остальных...):


template <typename Type>
class immutable_value<Type, false>
{
public:
    using value_type = Type;
    constexpr
    explicit
    immutable_value(const Type &value)
        : m_value(value)
    {
    }
    constexpr
    explicit operator Type() const
    {
        return value();
    }
    constexpr
    Type operator()() const
    {
        return value();
    }
    //    Base operator . () const
    //    {
    //        return value();
    //    }
    constexpr
    Type value() const
    {
        return m_value;
    }
private:
    const Type m_value;
};

Иммутабельные массивы


Пока вроде бы просто и неинтересно, но теперь примемся за массивы. Надо сделать что-то вроде std::array сохранив естественную семантику работы с массивом, в том числе для работы с STL (что может ослабить иммутабельность).


Особенность релизации заключается в том, что при обращении по индексу к многомерному возвращается массив меньшей размерности, тоже иммутабельный. Тип массива рекурсивно инстанцируется: см. operator[], а конкретные типы для итераторов и т.д выводятся с помощью array_traits.


namespace immutable {
    template <typename Tp>
    class array;

    template <typename ArrayType>
    struct array_traits;

    template <typename Tp, std::size_t Size>
    class array<Tp[Size]>
    {
        typedef       Tp* pointer_type;
        typedef const Tp* const_pointer;
    public:
        using array_type = const Tp[Size];
        using value_type = typename array_traits<array_type>::value_type;
        using size_type  = typename array_traits<array_type>::size_type;

        using iterator               = array_iterator<array_type>;
        using const_iterator         = array_iterator<array_type>;
        using const_reverse_iterator = std::reverse_iterator<const_iterator>;

        constexpr
        explicit
        array(array_type &&array)
            : m_array(std::forward<array_type>(array))
        {
        }

        constexpr
        explicit
        array(array_type &array)
            : m_array(array)
        {
        }
        ~array() = default;

        constexpr
        size_type size() const noexcept
        { return Size; }

        constexpr
        bool empty() const noexcept
        { return size() == 0; }

        constexpr
        const_pointer value() const noexcept
        { return data(); }

        constexpr
        value_type operator[](size_type n) const noexcept
        { return value_type(m_array[n]); } // рекурсивное инстанцирование для типа меньшей размерности

        constexpr
        value_type at(size_type n) const
        { return n < Size ? operator [](n) : out_of_range(); }

        const_iterator begin() const noexcept
        { return const_iterator(m_array.get()); }
        const_iterator end() const noexcept
        { return const_iterator(m_array.get() + Size); }

        const_reverse_iterator rbegin() const noexcept
        { return const_reverse_iterator(end()); }

        const_reverse_iterator rend() const noexcept
        { return const_reverse_iterator(begin()); }

        const_iterator cbegin() const noexcept
        { return const_iterator(data()); }

        const_iterator cend() const noexcept
        { return const_iterator(data() + Size); }

        const_reverse_iterator crbegin() const noexcept
        { return const_reverse_iterator(end()); }

        const_reverse_iterator crend() const noexcept
        { return const_reverse_iterator(begin()); }

        constexpr
        value_type front() const noexcept
        { return *begin(); }

        constexpr
        value_type back() const noexcept
        { return *(end() - 1); }
    private:
        constexpr
        pointer_type data() const noexcept
        { return m_array.get(); }

        [[noreturn]]
        constexpr
        value_type out_of_range() const
        { throw std::out_of_range("array: out of range");}
    private:
        const std::reference_wrapper<array_type> m_array;
    };
}

Для определения типа меньшей размерности используется класс характеристик:


namespace immutable {
    template <typename ArrayType, std::size_t Size>
    struct array_traits<ArrayType[Size]>
    {
        using value_type = std::conditional_t<std::rank<ArrayType[Size]>::value == 1,
                                              ArrayType,
                                              array<ArrayType> // immutable::array
                                             >;
        using size_type  = std::size_t;
    };
}

который для многомерных массивов для при индексировании возвращает иммутабельный массив меньшей размерности.


Операторы сравнения очень просты:


Операторы сравнения
template<typename Tp, std::size_t Size>
    inline bool
    operator==(const array<Tp[Size]>& one, const array<Tp[Size]>& two)
    {
        return std::equal(one.begin(), one.end(), two.begin());
    }

    template<typename Tp, std::size_t Size>
    inline bool
    operator!=(const array<Tp[Size]>& one, const array<Tp[Size]>& two)
    {
        return !(one == two);
    }

    template<typename Tp, std::size_t Size>
    inline bool
    operator<(const array<Tp[Size]>& a, const array<Tp[Size]>& b)
    {
        return std::lexicographical_compare(a.begin(), a.end(),
                                            b.begin(), b.end());
    }

    template<typename Tp, std::size_t Size>
    inline bool
    operator>(const array<Tp[Size]>& one, const array<Tp[Size]>& two)
    {
        return two < one;
    }

    template<typename Tp, std::size_t Size>
    inline bool
    operator<=(const array<Tp[Size]>& one, const array<Tp[Size]>& two)
    {
        return !(one > two);
    }

    template<typename Tp, std::size_t Size>
    inline bool
    operator>=(const array<Tp[Size]>& one, const array<Tp[Size]>& two)
    {
        return !(one < two);
    }

Иммутабельный итератор


Для работы с иммутабельным массивом используется иммутабельный итератор array_iterator:


namespace immutable {
    template <typename Tp>
    class array;

    template <typename Array>
    class array_iterator : public std::iterator<std::bidirectional_iterator_tag,
            Array> {
    public:
        using element_type = std::remove_extent_t<Array>;

        using value_type = std::conditional_t<
            std::rank<Array>::value == 1,
            element_type,
            array<element_type>
        >;

        using ptr_to_array_type = const element_type *;

        static_assert(std::is_array<Array>::value,
                      "Substitution error: template argument must be array");

        constexpr
        array_iterator(ptr_to_array_type ptr)
            : m_ptr(ptr)
        {
        }

        constexpr
        value_type operator *() const
        { return value_type(*m_ptr);}

        constexpr
        array_iterator operator++()
        {
            ++m_ptr;
            return *this;
        }

        constexpr
        array_iterator operator--()
        {
            --m_ptr;
            return *this;
        }

        constexpr
        bool operator == (const array_iterator &other) const
        {
            return m_ptr == other.m_ptr;
        }
    private:
        ptr_to_array_type m_ptr;
    };

    template <typename Array>
    inline constexpr
    array_iterator<Array> operator++(array_iterator<Array> &it, int)
    {
        auto res = it;
        ++it;
        return res;
    }

    template <typename Array>
    inline constexpr
    array_iterator<Array> operator--(array_iterator<Array> &it, int)
    {
        auto res = it;
        --it;
        return res;
    }

    template <typename Array>
    inline constexpr
    bool operator != (const array_iterator<Array> &a, const array_iterator<Array> &b)
    {
        return !(a == b);
    }
}
Отделение массивов от указателей сделано сознательно, несмотря на их близкое родство. 
В итоге, получим что-то вроде:

Пример кода с иммутабельным массивом
int x[5] = { 1, 2, 3, 4, 5 };
int y[5] = { 1, 2, 3, 4, 5 };

immutable::array<decltype(x)> a(x);
immutable::array<decltype(y)> b(y);

qDebug() << (a == b);

const char str[] = "abcdef";
immutable::array<decltype(str)> imstr(str);

auto it = imstr.begin();

while(*it)
    qDebug() << *it++;

Для многомерных массивов все тоже самое:


Пример с многомерным иммутабельным массивом
int y[2][3] = {
    { 1, 2, 3 },
    { 4, 5, 6 }
};
int z[2][3] = {
    { 1, 2, 3 },
    { 4, 5, 6 }
};

immutable::array<decltype(y)> b(y);
immutable::array<decltype(z)> c(z);

for(auto row = b.begin(); row != b.end(); ++row)
{
        qDebug() << "(*row)[0]" << (*row)[0];
}

for(int i = 0; i < 2; ++i)
    for(int j = 0; j < 2; ++j)
        qDebug() << b[i][j];

qDebug() << (b == c);

for(auto row = b.begin(); row != b.end(); ++row)
{
    for(auto col = (*row).begin(); col != (*row).end(); ++col)
        qDebug() << *col;
}

Иммутабельные указатели


Попробуем слегка обезопасить указатели. В этом разделе рассмотрим обычные указатели (raw pointers), а далее (сильно далее) рассмотрим smart pointers. Для smart pointers будет использоваться SFINAE.


По реализации immutable::pointer скажу сразу, что pointer не удаляет данные, не считает ссылки, а только обеспечивает неизменяемость объекта. (Если переданный указатель изменен или удален из-вне, то это нарушение контракта, которое средствами языка не отследить (стандартными средствами)). В конце-концов, защититься от умышленного вредительства или игры с адресами невозможно. Указатель должен быть корректно инициализирован.


immutable::pointer может работать с указателями на указатели любой степени ссылочности (скажем так).


Например:


Пример работы с иммутабельными указателями
immutable::pointer<QApplication*> app(&a);
app->quit();

char c = 'A';
char *pc = &c;
char **ppc = &pc;
char ***pppc = &ppc;

immutable::pointer<char***> x(pppc);
qDebug() << ***x;

Кроме вышеперечисленного, immutable::pointer не поддерживает работы со строками в стиле С:


const char *cstr = "test";
immutable::pointer<decltype(str)> p(cstr);

while(*p++)
      qDebug() << *p;

Данный код будет работать не так как ожидается, т.к. immutable::pointer при инкременте возвращает новый immutable::pointer с другим адресом, а в условном выражении будет проверяться результат инкремента, т.е. значение второго символа строки.


Вернемся к реализации. Класс pointer предоставляет общий интерфейс и, в зависимости от того что из себя представляет Tp (указатель на указатель или прото указатель) использует конкретную реализации pointer_impl.


 template <typename Tp>
    class pointer
    {
    public:
        static_assert( std::is_pointer<Tp>::value,
                       "Tp must be pointer");
        static_assert(!std::is_volatile<Tp>::value,
                      "Tp must be nonvolatile pointer");
        static_assert(!std::is_void<std::remove_pointer_t<Tp>>::value,
                      "Tp can't be void pointer");

        typedef Tp                                source_type;
        typedef pointer_impl<Tp>    pointer_type;
        typedef typename pointer_type::value_type value_type;

        constexpr
        explicit
        pointer(Tp ptr)
            : m_ptr(ptr)
        {
        }

        constexpr
        pointer(std::nullptr_t) = delete; // Перегрузка защищает от 0

        ~pointer() = default;

        constexpr
        const pointer_type value() const
        {
            return m_ptr;
        }

     /**
     * @brief operator = необязательное объявление, т.к const *const автоматически
     * запрещает присваивание.
     * При попытке присвоить, компиляторы дают несколько избыточных ошибок,
     * которые могут быть разбросаны по файлам и малоинформативны,
     * а явное описание " = delete" приводит к тому, что диагностируется
     * только одна конкретная ошибка
     */
        pointer& operator=(const pointer&) = delete;

        constexpr /*immutable<value_type>*/
        value_type operator*() const
        {
            return *value();
        }

        constexpr
        const pointer_type operator->() const
        {
            return value();
        }

        // добавим неоднозначности 
        template <typename T>
        constexpr
        operator T() = delete;

        template <typename T>
        constexpr
        operator T() const = delete;
    /**
     * @brief operator [] не реализован сознательно, чтобы не смешивать массивы
     * и указатели.
     *
     * Использование типов-аргументов по-умолчанию помогают компилятору
     * дать более короткое и конкретное сообщение об ошибке
     * (использовании удаленной функции)
     * @return
     */
        template <typename Ret = std::remove_pointer_t<Tp>, typename IndexType = ssize_t>
        constexpr
        Ret operator[](IndexType) const = delete;

        constexpr
        bool operator == (const pointer &other) const
        {
            return value() == other.value();
        }
        constexpr
        bool operator < (const pointer &other) const
        {
            return value() < other.value();
        }
    private:
        const pointer_type m_ptr;
    };

Суть следующая: был тип T , а для его хранения/представления используется (шаблонно-рекурсивно) реализация pointer_impl<T , true>, что можно изобразить так:


pointer_impl<T***, true>{
      pointer_impl<T**, true>
      {
          pointer_impl<T*, false>
          {
              const T *const
          }
      }
}

Итого, получается: const T const const *const.


Для простого указателя (который не указывает на другой указатель) реализация следующая:


    template <typename Type>
    class pointer_impl<Type, false>
    {
    public:
        typedef std::remove_pointer_t<Type> source_type;
        typedef source_type *const          pointer_type;
        typedef source_type                 value_type;

        constexpr
        pointer_impl(Type value)
            : m_value(value)
        {
        }

        constexpr
        value_type operator*() const noexcept
        {
            return *m_value;
            //     * для обычных указателей
        }

        constexpr
        bool operator == (const pointer_impl &other) const noexcept
        {
            return m_value == other;
        }

        constexpr
        bool operator < (const pointer_impl &other) const noexcept
        {
            return m_value < other;
        }

        constexpr
        const pointer_type operator->() const noexcept
        {
            using class_type = std::remove_pointer_t<pointer_type>;

            static_assert(std::is_class<class_type>::value || std::is_union<class_type>::value ,
                          "-> used only for class, union or struct");
            return m_value;
        }

    private:
        const pointer_type m_value;
    };

Для вложенных указателей (указатели на указатели):


    template <typename Type>
    class pointer_impl<Type, true>
    {
    public:
        typedef std::remove_pointer_t<Type>             source_type;
        typedef pointer_impl<source_type> pointer_type;
        typedef pointer_impl<source_type> value_type;

        constexpr
        /* implicit */
        pointer_impl(Type value)
            : m_value(*value)
        {   //        /\ remove pointer
        }

        constexpr
        bool operator == (const pointer_impl &other) const
        {
            return m_value == other; // рекурсивное инстанцирование
        }

        constexpr
        bool operator < (const pointer_impl &other) const
        {
            return m_value < other; // рекурсивное инстанцирование
        }

        constexpr
        value_type operator*() const
        {
            return value_type(m_value); // рекурсивное инстанцирование
        }

        constexpr
        const pointer_type operator->() const
        {
            return m_value;
        }

    private:
        const pointer_type m_value;
    };

Что не надо делать!

Для следующих видов указателей особого смысла не стоит делать специализации:


  • указатель на массив (*)[];
  • указатель на функцию(*)(Args… [...]);
  • указатель на переменную класса, Class:: весьма специфичная вещь, нужна при "колдовстве" с классом, нужно связывать с объектом;
    -указатель на метод класса (Class::
    )(Args… [...]) [const][volatile].

Иммутабельные smart pointers


Как определить что перед нами smart pointer? Smart pointers реализуют операторы * и ->. Чтобы определить их наличие воспользуемся SFINAE (реализацию SFINAE рассмотрим позже):


namespace immutable
{
    // is_base_of<_Class, _Tp>
    template <typename Tp>
    class is_smart_pointer {
        DECLARE_SFINAE_TESTER(unref, T, t, t.operator*());
        DECLARE_SFINAE_TESTER(raw,   T, t, t.operator->());
    public:
        static const bool value = std::is_class<Tp>::value
                                && GET_SFINAE_RESULT(unref, Tp)
                                && GET_SFINAE_RESULT(raw, Tp);
    };
}

Скажу сразу, что через operator ->, увы, используя косвенное обращение, можно нарушить иммутабельность, особенно если в классе есть mutable данные. Кроме того константность возвращаемого значения может быть снята, как компилятором (при выводе типа), так и пользователем.


Реализация – здесь все просто:


namespace immutable
{
    template <typename Type>
    class smart_pointer {
    public:
        constexpr
        explicit
        smart_pointer(Type &&ptr) noexcept
            : m_value(std::forward<Type>(ptr))
        {

        }
        constexpr
        explicit
        smart_pointer(const Type &ptr)
            : m_value(ptr)
        {

        }

        constexpr
        const auto operator->() const
        {
            const auto res = value().operator->();
            return immutable::pointer<decltype(res)>(res);// in C++17 immutable::pointer(res);
        }

        constexpr
        const auto operator*() const
        {
            return value().operator*();
        }

        constexpr
        const Type value() const
        {
            return m_value;
        }
    private:
        const Type m_value;
    };
}

SFINAE


Что это такое и с чем его едят лишний раз объяснять не надо. С помощью SFINAE можно определить наличие в классе методов, типов-членов и т.д, даже наличие перегруженных функций (если задать в выражении testexpr вызов нужной функции с необходимыми параметрами). arg может быть пустым и не участвовать в testexpr. Здесь используется SFINAE с типами и SFINAE с выражениями:


#define DECLARE_SFINAE_BASE(Name, ArgType, arg, testexpr)   \
    typedef char SuccessType;                               \
    typedef struct { SuccessType a[2]; } FailureType;       \
    template <typename ArgType>                             \
    static decltype(auto) test(ArgType &&arg)               \
            -> decltype(testexpr, SuccessType());           \
    static FailureType test(...);

#define DECLARE_SFINAE_TESTER(Name, ArgType, arg, testexpr) \
struct Name {                                               \
    DECLARE_SFINAE_BASE(Name, ArgType, arg, testexpr)       \
};

#define GET_SFINAE_RESULT(Name, Type) (sizeof(Name::test(std::declval<Type>())) == \
                                       sizeof(typename Name::SuccessType))

И еще: перегрузку можно разрешить (найти нужную перегруженную функцию) если сигнатуры совпадают, но отличаются квалификатором const [ volatile ] или volatile совместно с SFINAE в три фазы:


1) SFINAE — если есть, то ОК
2) SFINAE + QNonConstOverload, если не получилось, то
3) SFINAE + QConstOverload


В исходниках Qt можно найти интересную и полезную вещь:


Разрешение перегрузки с const
    template <typename... Args>
    struct QNonConstOverload
    {
        template <typename R, typename T>
        Q_DECL_CONSTEXPR auto operator()(R (T::*ptr)(Args...)) const Q_DECL_NOTHROW -> decltype(ptr)
        { return ptr; }

        template <typename R, typename T>
        static Q_DECL_CONSTEXPR auto of(R (T::*ptr)(Args...)) Q_DECL_NOTHROW -> decltype(ptr)
        { return ptr; }
    };

    template <typename... Args>
    struct QConstOverload
    {
        template <typename R, typename T>
        Q_DECL_CONSTEXPR auto operator()(R (T::*ptr)(Args...) const) const Q_DECL_NOTHROW -> decltype(ptr)
        { return ptr; }

       template <typename R, typename T>
        static Q_DECL_CONSTEXPR auto of(R (T::*ptr)(Args...) const) Q_DECL_NOTHROW -> decltype(ptr)
        { return ptr; }
    };

    template <typename... Args>
    struct QOverload : QConstOverload<Args...>, QNonConstOverload<Args...>
    {
        using QConstOverload<Args...>::of;
        using QConstOverload<Args...>::operator();
        using QNonConstOverload<Args...>::of;
        using QNonConstOverload<Args...>::operator();

        template <typename R>
        Q_DECL_CONSTEXPR auto operator()(R (*ptr)(Args...)) const Q_DECL_NOTHROW -> decltype(ptr)
        { return ptr; }

        template <typename R>
        static Q_DECL_CONSTEXPR auto of(R (*ptr)(Args...)) Q_DECL_NOTHROW -> decltype(ptr)
        { return ptr; }
    };

Итог


Попробуем что получилось:


QPoint point(100, 500);
Immutable<QPoint> test(point);

test().setX(1000); // не поменяет исходный объект
qDebug() << test().isNull() << test().x() << test().y();

int x[] = { 1, 2, 3, 4, 5 };
Immutable<decltype(x)> arr(x);
qDebug() << arr[0];

Операторы


Давате вспомним про операторы! Например, добавим поддержку оператора сложения:
Сначала реализуем оператор сложения вида Immutable<Type> + Type:


template <typename Type>
inline constexpr
Immutable<Type> operator+(const Immutable<Type> &a, Type &&b)
{
    return Immutable<Type>(a.value() + b);
}

В С++17 вместо


 return Immutable<Type>(a.value() + b); 

можно записать


return Immutable(a.value() + b);

Т.к. оператор + коммутативен, то Type + Immutable<Type> можно реализовать в виде:


template <typename Type>
inline constexpr
Immutable<Type> operator+(Type &&a, const Immutable<Type> &b)
{
    return b + std::forward<Type>(a);
}

И снова, через первую форму реализуем Immutable<Type> + Immutable<Type>:


template <typename Type>
inline constexpr
Immutable<Type> operator+(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return a + b.value();
}

Теперь можем работать:


Immutable<int> a(1), b(2);

qDebug() << (a + b).value()
         << (a + 1).value()
         << (1 + a).value();

Аналогично можно определить остальные операции. Вот только не надо перегружать операторы получения адреса, &&, ||! Унарные +, -, !, ~ могут пригодиться… Эти операции наследуются: (), [], ->, ->, (унарный).


Операторы сравнения должны возвращать значения булевского типа:


Операторы сравнения
template <typename Type>
inline constexpr
bool operator==(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return a.value() == b.value();
}

template <typename Type>
inline constexpr
bool operator!=(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return !(a == b);
}

template <typename Type>
inline constexpr
bool operator>(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return a.value() > b.value();
}

template <typename Type>
inline constexpr
bool operator<(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return b < a;
}

template <typename Type>
inline constexpr
bool operator>=(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return !(a < b);
}

template <typename Type>
inline constexpr
bool operator<=(const Immutable<Type> &a, const Immutable<Type> &b)
{
    return !(b < a);
}
Tags:
Hubs:
+15
Comments 123
Comments Comments 123

Articles