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

Привет, Хабр! Об иммутабельных данных немало говориться, но о реализации на С++ найти что-то сложно. И, потому, решил данный восполнить пробел в дебютной статье. Тем более, что в языке 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);
}
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 123

    +3
    Спасибо за статью, радикальный подход!
    Проясните, пожалуйста, следующий момент:
    test().setX(1000); // не поменяет исходный объект
    

    Что такое setX на неизменяемом объекте? Какой объект поменяется? Я ожидал увидеть здесь ошибку компиляции, так как не константные методы не должны работать на immutable.
      +1
      создаётся копия объекта, для копии вызывается setX, копия выходит из контекста и удаляется.
        +2
        А зачем? Может стоит возвращать константную ссылку, чтобы подобного не происходило? Или это замысел такой?
          0
          Могу предположить, что это чтобы легче было использовать парадигмы ФП в с++. Зачем? Спросите автора, я не знаю ;)
            0
            Где нужна иммутабельность? Например, в паралелльном программировании — иммутабельность уменьшает количество побочных эффектов. ФП в С++ — boost, в Qt активно используется в фьючерсах, там есть известная концепция: ReduceMap и тд.
              0
              Перед тем, как заявлять иммутабельность, стоило бы посмотреть, в какой код преобразуется ваш шаблон, и какие машинные коды из него получатся. \Fa в студии и -S в мингве.

              И не забывайте, 99% не заморачиваются const_cast. В лучшем случае сразу reinterpret_sast, но чаще просто (T*).
            0
            помните про const_cast
              0

              помните про reinterpret_cast, который вертел ваши объекты как ему хочется

            0
            Это же темная комната с детскими грабельками.
            –1
            Смысл в сохранении естественной семантики работы с объектом. setX может что-то вернуть и это что-то может использоваться далее. В С++ пока нету перегрузки оператора. (пока нету, может быть в середине марта будет решение), который позволит реализовывать умные ссылки, поможет делать защитные заместители и декораторы.
            +4
            Я честно говоря не понял в чем преимущества

            Immutable<int> a(1);
            

            перед банальным (и отлично работающим)

            const int a(1);
            
              +1

              Насколько я понял, разница проявляется при передаче объекта по ссылке в подпрограмму.


              const просто запрещает подпрограмме изменять объект — но не дает никаких гарантий. Immutable дает гарантию подпрограмме что снаружи объект также никто не изменит.

                –1
                Чукча не дуракКомпилятор оптимизирующий, статические константные величины просто будут подставляться в качестве значения. В худшем случае, поместит в секцию .data. А от прямой инъекции в твою память тебя практически ничто не спасёт.
                  0

                  При чем тут инъекция в память?

                    0

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

                      0

                      Зачем менять значение, которое декларировано как неизменяемое?

                        0
                        так а если незачем, то «зачем платить больше» и копировать то, что не собираешься изменять?
                          0

                          А кто говорил о копировании? В том-то и смысл, что там где обычную структуру надо копировать — на Immutable-версию можно передавать ссылку.

                            0

                            А теперь посмотрите внимательно на приведённый в публикации код, а именно на operator(), который возвращает копию объекта.


                            Зачем передавать ссылку на Immutable, а затем при каждом обращении к нему делать копию объекта, если копию можно сделать только один раз, передавая аргументы в функцию по значению? Just KISS.


                            Кстати, просто передавая константный объект по ссылке, у нас есть возможность вызывать методы без копирования объекта.

                              0

                              Да там вообще много глупостей. Я отвечал на вопрос "зачем", а не доказывал идеальность конкретной реализации.

                              –5
                              еще раз: есть такая нехорошая вешь как const_cast. Посмотрите «Язык программирования D» и перечитайте введение
                              0

                              Незачем. Но если язык позволяет выстрелить себе в ногу, это стоит иметь в виду.

                            –1
                            Во-первых, любая попытка изменения константы этапа компиляции — UB. Тут и говорить не о чем.
                            Во-вторых, есть серьёзные сомнения, что компилятор сможет правильно переварить всю эту мешанину на серьёзных классах. Что получится в итоге, ванговать не имеет смысла
                            В-третьих, я не вижу T* operator&() = delete;, так что ничто не помешает навернуть константность через (void*) (Кроме стандарта, но разве он написан не для того, чтобы мы его нарушали?)
                            В-четвёртых, даже если выполнить предыдущий пункт, всё ещё можно прогибать на синонимичный шаблон без const через reinterpret_cast.
                            В-пятых, для меня до сих пор не ясно, каким местом это безопаснее и нагляднее, чем const& или const*const. Ну да, выглядит дишнее. Ну да, типа нагляднее. И? Мы пришли на плюсах программировать или делать из них очередное manageбожество?! Быть может, не стоит городить то, что всё равно не сработает?
                              –1

                              1) иммутабельные данные могут обрабатываться в разных потоках без проблем, за счет меньшего числа побочных эффектов.
                              2) может. Такое устроит (это из будущей статьи):


                              QVector<Immutable<int>> imm = {
                                         Immutable<int>(1),
                                         Immutable<int>(2),
                                         Immutable<int>(3),
                                         Immutable<int>(4),
                                         Immutable<int>(5),
                                     };
                              
                              Stream<decltype(imm)> t(imm);
                              qDebug() << t.map(static_cast<QString(*)(int, int)>(&QString::number), 2)
                                          .filter(std::bind(
                                                      std::logical_not<>(),
                                                      std::bind(QString::isEmpty, std::placeholders::_1))
                                                  )
                                          .map(&QString::length)
                                          .filter(std::greater<>(), 1)
                                          .filter(std::bind(
                                                      std::logical_and<>(),
                                                      std::bind(std::greater_equal<>(), std::placeholders::_1, 1),
                                                      std::bind(std::less_equal<>(),    std::placeholders::_1, 100)
                                                      )
                                                  )
                                          .reduce(std::multiplies<>(), 1);

                              3) в конце статьи упомянуто, что такое не надо делать;
                              4) манипулируя с адресом можно сделать все что угодно. Даже в Java через механизм рефлексии можно напакостить;
                              5) меньше побочных эффектов, т.к. работаете с копией.

                                0
                                2) А можно мне не факт компиляции, а сравнение сгенерированного ассемблерного кода для вашей версии и классического подхода?
                                5) Спасаясь от мутабельности вы нарываетесь на иммутабельность. А я тем временем всё равно не знаю, чем ваша мешанина лучше, чем
                                int a = 5;
                                const int& b = a;
                                

                                http://ideone.com/sOcMvB
                                Всё, дошло.
                                  0
                                  5) Ответ самому себе, Immutable сохраняет своё состояние, но, при этом, плодит уймы своих изменённых копий, которые, конечно, могли бы быть выоптимизированны компилятором, но даже если это и произойдёт, на времени построения и перепостроения это скажется фатальным образом.
                                  В целом подход имеет право быть, но я бы не заморачивался константностью внутри класса, и просто бы перегрузил все операторы для Immutable поведения, оставив возможность работать с хранимым значением напрямую.
                                    0

                                    к 2) оверхед не очень большой, но… давайте обсудим с цифрами. Предоставьте, пожалуйста код, на Ваше усмотрение (для большей объективности), чтобы для него получить выход компилятора.

                                      0
                                      Зачем его представлять, если вы предлагаете постоянно копировать объекты?
                                  0
                                  откуда взяться этим побочным эффектам? У вас есть 6 базовых способа передать значение в функцию (=, &, const&, *, const*, &&), их все же не просто так придумали, а для того, чтобы функция работала с данными простым, понятным и максимально эффективным способом
                            0
                            «const просто запрещает подпрограмме изменять объект — но не дает никаких гарантий. Immutable дает гарантию подпрограмме что снаружи объект также никто не изменит.»

                            Вы не могли бы пояснить свою мысль? Кто может снаружи изменить объект? Другой тред?
                              –3
                              Смотрите, вы передаете в другую функцию: внутри можно изменить с помощью const_cast, игры с адресами и т.д. При работе с Immutable вы работаете с копией, а не с оригиналом. Прочитайте главу 8.1 «Язык программирования D», автор А. Александреску.
                              Иммутабельные данные в параллельном программировании в разы снижают количество ошибок, за счет меньшего числа побочных эффектов.
                                +2
                                Не лучше ли просто писать нормальные программы где не используют подобные хаки? Тогда компилятор вежливо предупредит Вас что Вы пытаетесь передать иммутабельный объект в функцию которая может его изменить, а Вы всегда можете поставить в этом месте создание копии, если подобное решение допустимо. Да, это порождает необходимость расстановки кучи const-ов везде где это нужно. Но зато в итоге и получается намного более надежный и понятный код. Ваше решение пытается создавать копии автоматически, но мне честно говоря кажется что это плохая идея. Копий будет создано больше чем нужно, некоторые ошибки в норме выявимые еще на этапе компиляции очень странно проявят себя в рантайме, да и в целом довольно громоздко.
                                  +2
                                  При работе с Immutable вы работаете с копией, а не с оригиналом

                                  Так чем это лучше простой передачи по значению?


                                  Главное отличие immutable от const: преобразование &T -> const &T можно сделать неявно, а &T -> immutable &T — нет. Собственно, автор и попытался сделать враппер для эмуляции функционала D.

                                  +1

                                  Обратный вызов. Или сама функция:


                                  void foo(const int& a, int& b) {
                                    b = 42;
                                    std::cout << a << std::endl;
                                  }
                                  
                                  //...
                                  
                                  int a = 5;
                                  foo(a, a); // Сюрприз!
                                    0

                                    Не понял к чему это: вы меняете неконстантную ссылку

                                      +3

                                      Внутри foo ссылка a — константная. Но это не помешало ей внезапно измениться в процессе выполнения foo.

                                        –2
                                        Вы неверно понимаете модификатор const. В Вашем примере есть не константная переменная a и функция foo которая обещает не менять первый из аргументов получаемый по ссылке. Если Вы хотите сделать «иммутабельный объект», то пишете

                                        const int a = 5;
                                        foo(a, a); // Действительно сюрприз, причем от компилятора!


                                        Модификатор const у функции лишь говорит что иммутабельный объект может использоваться в качестве первого аргумента функции. Он не защищает (и не должен защищать) от внешнего по отношению к функции изменению объекта (в данном примере таковым является вызов функции, а не ее тело).
                                          0

                                          А я что говорю?

                                            0
                                            Если Вам нужен иммутабельный объект, то Вы объявляете этот объект иммутабельным. Const int как в моём примере — и всё, дальше Вы получили все гарантии.
                                            Объявление же аргумента функции const не делает лежащий за ним объект иммутабельным.
                                              0

                                              А что делать если функция должна принять иммутабельный параметр по ссылке-то?

                                                –1
                                                Функция никак не может гарантировать иммутабельность объекта который она принимает, да и не должна, вообще говоря. Иммутабельность тех или иных объектов — часть архитектуры приложения. К примеру если какой-то объект обязательно должен быть иммутабельным, то мы можем запретить его прямое создание заставив пользователя пользоваться фабрикой или встроенными функциями-конструкторами которые будут возвращать константные ссылки. Если же вдруг в приложении творится бардак в силу чего есть некий подозрительный объект который непонятно кто и когда его может изменить и этот объект хочется «превратить» в иммутабельный, то достаточно сделать ОДНУ его копию, помеченную как иммутабельную вместо того чтобы городить огород с обертками

                                                void foo(const int& a, int& b) {
                                                  const immutable_a(a); // черт его знает что нам передали
                                                  b = 42;
                                                  std::cout << immutable_a << std::endl;
                                                }
                                                
                                                //...
                                                
                                                int a = 5;
                                                foo(a, a); // Работает!
                                                
                                                  0

                                                  А если требуется избежать копирования?..

                                                    0
                                                    Иммутабельность нужно обеспечить лишь один раз. При грамотно спроектированном приложении у любого иммутабельного объекта жизнь четко делится на две половины: инициализация где объект не расшарен и иммутабельное состояние где объект собственно и используется по назначению. Нам достаточно проследить чтобы интерфейс обеспечивающий передачу объекта из первой половины во вторую допускал передачу объекта только в виде константных ссылок. Как правило никаких сложностей с этим не возникает. При таком подходе ничего лишний раз никогда не копируется — в отличие от подхода автора, к слову, в котором копии легким движением руки порождаются на каждый чих. У нас на подобных иммутабельных объектах все приложение обрабатывающее данные в реальном времени построено, поверьте: оно там все отлично работает, const в C++ умные люди придумали. Если же в приложении бардак и требуется зачем-то постоянно превращать не-immutable состояние в immutable, то таки да, придется плодить копии. Но собственно, immutable<> шаблон в этом ничего не меняет.
                                                      +1

                                                      Вы все еще подходите со стороны вызывающего кода. Да, там достаточно const.


                                                      Но для вызываемого кода константная ссылка не гарантирует иммутабельность.

                                                        –2
                                                        Иммутабельность — свойство объекта, а не функции которая с ним работает.
                                                        Объект предоставляет вызывающий код, ему и обеспечивать его иммутабельность.
                                                        Собственно шаблон автора ничего в этом подходе не меняет. От слова «совсем». Экземпляр immutable<> будет создавать вызывающий код и при этом переход от «обычного» объекта к immutable потребует создания копии
                                                          0

                                                          Мои, самые вдумчивые читатели, верным путем идете, товарищи, вот пища для ума, основанная на Ваших примерах:


                                                          void foo(const Immutable<int> &a, Immutable<int> &b)
                                                          {
                                                              a = 42;
                                                              b = 42;
                                                              a = b;
                                                              b = a;
                                                          
                                                              const_cast<Immutable<int>&>(a) = 100;
                                                              a = Immutable<int>(500);
                                                              b = Immutable<int>(500);
                                                              a = std::move(Immutable<int>(500));
                                                              b = std::move(Immutable<int>(500));
                                                          }

                                                          Каждое присваивание даст ошибку компиляции. Шаблон Immutable<> дает еще один уровень защиты.
                                                          Можно один раз объект обернуть в Immutable<>, а дальше использовать по ссылке и никаких лишних копирований.

                                                            +1

                                                            Лишние копирования у вас будут при каждом вызове оператора (). И еще в конструкторе.

                                                              –1
                                                              Уважаемый ixjxk, я прекрасно понимаю Вашу идею «дополнительного уровня защиты». Вы заставляете вызывающий код создавать immutable-копию каждый раз когда происходит переход от «обычного» кода к коду работающему с immutable-данными, но если весь код работающий дальше использует Ваш шаблон, то дополнительного копирования после этого первого не происходит. Там есть определенные косяки с сеттерами (по хорошему весь класс следовало бы банально сделать эквивалентом const X&) но в целом он работает. Это все понятно и меня честно говоря уже начали раздражать люди которые повторяют одни и те же вещи «человеку не понявшему идеи». Да все я прекрасно понял, спасибо. Для меня эта тема весьма актуальна поскольку, повторю, у меня есть приложение которое обрабатывает гигабайты данных в реальном времени в двадцать потоков которые их шарят между собой. И Вы знаете, за 3 года разработки в команде из 20 человек случаев когда const-защиты не хватило бы не было ни одного. Все они отлавливались компилятором, очень удобно было, причем код кое-где пришлось переписать существенно. Вот с mutable-данными проблемы были и мы, кстати, придумали как довольно неплохо защитить от случайных ошибок и их (правда, уже не на уровне компиляции, а в реал-тайме).

                                                              Так вот, возвращаясь к нашим баранам: на основании своего опыта я пытаюсь сказать одну простую, в общем-то, вещь. Функции (да и объекты) не существуют сами по себе, «в вакууме». Они являются частями приложения. И у этого приложения должна быть структура. Это включает в себя внятное понимание того какие данные есть в приложении, как их организовать наиболее удобным образом в объекты и как происходит обработка этих объектов. И уже под эту структуру пишется собственно код. Вопросы времени жизни объекта и того шарится ли объект между разными потоками или нет естественным образом являются частью этой структуры и как правило естественным образом решаются в ее рамках. И тогда описанные выше проблемы которые Вы пытаетесь решать, на уровне функций уже просто не возникают. И оказывается возможным не копировать объекты вообще (у Вас они копируются минимум один раз).

                                                              C++ в этом отношении довольно специфичный язык. Он очень сильно заточен на то чтобы работать с приложениями наделенными подобной структурой. Если же подобной структуры в приложении нет то… в плюсах есть сто тысяч и один способ выстрелить себе в ногу и те кто пытаются халтурить с плюсовым кодом быстро познают эту истину. А затем начинается попытка «исправить плохой C++ шаблонами» дописав туда функциональность которая один-два подобных способа перекрывает. Мне это представляется плохой идеей, блуждая вслепую Вы наступите не на одни грабли так на другие. Для написания подобного «бесструктурного» кода лучше подходит C#, а не C++ и холивар на тему GC тому ярким свидетельством: «умные указатели» в плюсах как и все остальное сильно увязано на наличие у приложения структуры.
                                                                –1

                                                                Предчувствую очередной минус, но для 0serg за его труд и аргументированность отвечу.


                                                                1) Цель статьи рассказать про концепт ФП — иммутабельные данные. Про Map, Reduce, Filter, карринг и т.п. много написано, но про иммутабельные данные, которые в ФП являются одним из краеуголных камней, сложно что-то найти. Про операции над списками много статей, а реализации некоторых дают немалый оверхед.


                                                                2) ФП в С++ смотрится странно, а некоторые его концепты вызывают недоумение. Но и они находят свое примение.


                                                                3) D не мой основной язык, просто в стиле "Практический подход к решению проблем программирования C++" (Автор: Мэтью Уилсон) предложил как можно реализовать immutable.


                                                                4) Почему-то все комментарии прочитать про D заминусованы, хотя из D в С++ уже пришли шаблоны с переменным числом аргументов, static_assert. В С++ 17 придет constexpr if. Да, то из D, в адаптированной для С++ форме.
                                                                В книге "Язык программирования D" от Александреску очень хорошо написано про иммутабельные данные (глава 8).


                                                                5) Теперь по сути. Все стали критиковать недостатки value. А достоинства шаблона Immutable<> как-то ускользнули. К value я вернусь попозже.
                                                                Например, использование к указателям.


                                                                void foo(Immutable<int*> &a)
                                                                {
                                                                    int *c = new int(100500);
                                                                    Immutable<int*> b(c);
                                                                    *a = 0; // error
                                                                    a = b; //error
                                                                    //...
                                                                }

                                                                Оверхед от Immutable<> меньше чем от smart pointers. А для компиляторов, которые устраняют лишнее копирование (стандартное требование в С++17), его либо нет, либо очень мал. Потому что, шаблонная обертка выкидывается Gimplifier (это под капотом gcc) в gcc.


                                                                6) Почему оставлены модифицирующие функции для value?


                                                                • Функция может что-то возвращать что-то интересное для нас (заминусовали);
                                                                • Сохранение семантики (заминусовали);
                                                                • Некоторый контекст (какая-нибудь библиотека) может потребовать наличия данной функции;
                                                                • Некоторые функции в какой-то библиотеке (которую нельзя менять) не меняют объект (явно), но не объявлены константными (разгильдяйство, старый код и т.д.)
                                                                • В общем случае эти функции сложно запретить (не будем же нежелательные функции убирать в private и там писать using Class::function). Более подробно расскажу в примере с заместителем:
                                                                • нужно сделать класс (защитный заместитель), или еще лучше что-то вроде шаблона optional (C++ 17, см. предложения Страуструпа), который должен быть похож на замещаемый класс. Вот здесь в дело и идет CRTP плюс перегрузка оператора. (за его отсутствие у меня используется ()).
                                                                  Если нужны более объемные примеры, то поищу (у меня сейчас делается еще одна статья, плюс готовится цикл статей по написанию собственного компилятора в доступном изложении).
                                                                  0

                                                                  Мне кажется, что минусуют, потому что получилось очень и очень криво. Нужно провести большую работу над ошибками.


                                                                  Вот лично мне вот этот кусок не нравится:


                                                                      int *c = new int(100500);
                                                                      Immutable<int*> b(c);

                                                                  Объясняю: после объявления immutable<int*> b(c) у нас остался доступ к значению через c. Здесь никакой иммутабельностью и не пахнет. Можно было бы просто написать const int *b = c;


                                                                  Единственный допустимый вариант:


                                                                      Immutable<int*> b(100500);

                                                                  6) Почему оставлены модифицирующие функции для value?

                                                                  Явное лучше неявного. Если функция возвращает что-то интересное, но может изменить значение, программист должен явно написать что-то типа b.copy().some_method();.


                                                                  Также мне не нравится здесь CRTP. Понимаю, что он сделан для замены оператора точка, но без него можно прекрасно обойтись. Есть оператор ->, который удобно использовать, когда оборачивается указатель (не меняется семантика). Если же оборачивается значение, тогда придётся менять . на ->. Но смысла в оборачивании значения нет, т.к. переменная будет копироваться при вызове функции — значит, можно просто передать аргумент по значению без всяких извратов.

                                                                    0

                                                                    Не важно сколько интересного возвращает функция, но если она меняет данные — ее нельзя вызывать на неизменяемых данных. Это очевидно.


                                                                    Аналогично с библиотеками. Если библиотека требует наличия функции, которая меняет данные — значит, она не поддерживает неизменяемые данные.


                                                                    Легаси и разгильдяйство прекрасно лечится при помощи const_cast. Да, const_cast есть зло — но это зло хотя бы будет локализовано в проблемных местах, а не будет заложено в архитектуру, как это сделали вы.


                                                                    Запретить такие функции — проще простого. Надо просто добавить const!

                                                                      0
                                                                      Оверхед от Immutable<> меньше чем от smart pointers.


                                                                      unique_ptr как правило не имеет оверхеда вообще (покрывая при этом более 90% потребностей)
                                                                      shared_ptr мало где нужен и при правильном использовании имеет хотя и не-нулевой, но пренебрежимо малый по сравнению с временем на копирование сколь-либо крупного объекта оверхед

                                                                      А для компиляторов, которые устраняют лишнее копирование (стандартное требование в С++17), его либо нет, либо очень мал


                                                                      В общем случае Вы не можете сделать потенциально mutable объект immutable не создав его копию (т.к. исходный объект может кто-то в любой момент изменить). Поэтому оверхед на создание копии которая будет храниться в immutable<> будет всегда. С остальным кодом порождающим копии — как повезет.

                                                                      Цель статьи рассказать про концепт ФП — иммутабельные данные.


                                                                      Так я не против иммутабельных данных, я их очень широко использую и люблю. Вот отличный иммутабельный int:

                                                                      const int i = 5;
                                                                      


                                                                      Я пытаюсь понять есть ли практические сценарии где может быть востребовано автоматическое порождение immutable-данных шаблоном типа Вашего вместо явного порождения immutable-объекта в коде, но к сожалению никто из минусующих не удосужился хотя бы одного подобного примера привести.
                                                                        0
                                                                        В общем случае Вы не можете сделать потенциально mutable объект immutable не создав его копию (т.к. исходный объект может кто-то в любой момент изменить)

                                                                        ничто не мешает написать make_immutable
                                                                          0
                                                                          В момент превращения объекта в immutablе Вам где-то нужно взять гарантию что все вышележащие уровни, другие потоки и т.п. не имеют mutable-ссылок на этот объект и не будут их использовать. Но если у Вам есть такая гарантия (например от архитектуры) то и шаблон уже не нужен, достаточно объект передавать дальше как const &.
                                                                            0
                                                                            Это не пример практического сценария, это искусственно сконструированный пример проблемы. Хотите я Вам подобный искусственный пример для неаккуратной работы с умными указателями накидаю? При этом при правильном использовании умные указатели работают великолепно и дополнения самописными велосипедами не требуют.
                                                                              0

                                                                              Нет, это упрощенный пример реальной проблемы, с которой я столкнулся 10 лет назад.

                                                                                0
                                                                                Рискну предположить что Ваша проблема несложно и надежно решалась без нужды в шаблонах типа immutable<>. Потому и спрашиваю что хочу разобраться, так ли это, или есть нетривиальный сценарий который стоит предусмотреть и перекрыть.
                                                                                  0

                                                                                  Да не решается она без прописывания иммутабельности в определении функции! Тут разве что restrict поможет, но его в стандарте нет.

                                                                                    0

                                                                                    Да, не решается. Но речь вот о чём: я (да и многие другие) ещё не встречал ситуацию, где настолько была бы необходима иммутабельность. Видимо, на C++ люди пишут аккуратно, и им достаточно обычного const.


                                                                                    С иммутабельностью (readonly поля) же я намучался в C#, когда компилятор неявно делал копирование структуры против моей воли, даже не выдавая предупреждений.

                                                                                      0
                                                                                      Иммутабельность очень удобна для параллельной обработки, особенно в модели акторов, там это действительно сильно помогает. Но const там за глаза хватает если им грамотно пользоваться
                                                                                        0

                                                                                        Не встречал такого поведения компилятора в C#. Можно пример? Кстати, readonly поля — это еще не иммутабельность.

                                                                                          +1

                                                                                          Можно.


                                                                                          class SomeClass<T>
                                                                                             where T : SomeInterface
                                                                                          {
                                                                                              readonly T field;
                                                                                          }
                                                                                          

                                                                                          Если T — value type, то при вызове методов интерфейса у field каждый раз будет неявно создаваться копия объекта, а не вываливаться ошибка, что так делать нельзя. Если убрать readonly, то всё будет корректно.

                                                                                            –1

                                                                                            Но это же логично. Какого же еще поведения вы ожидали вызывая методы у readonly-структуры?..

                                                                                              +1

                                                                                              Ожидал увидеть ошибку компиляции, что нельзя так делать.
                                                                                              Почему readonly-объект в C# можно менять, выззывая метода, а структуру нет? Нелогично.

                                                                                                0

                                                                                                Вы упускаете тот факт, что компилятор не знает является ли тип T ссылочным или значимым...

                                                                                                  +1

                                                                                                  Да, не знает, потому T — generic без class/struct constraint. Но можно было бы просто сделать ошибкой использование readonly в сочетании с generic-типами, не имеющих ограничений.

                                                                                                    –1

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

                                                                                                      +1

                                                                                                      Да, в C# нет разделения методов на const и не-const. А в C++ эта проблема решается одним модификатором const.

                                                                                        –1
                                                                                        Я приводил пример решения, он тривиален: если бы объект был изначально объявлен const, то проблему в Вашем примере отловил бы еще компилятор. И для иммутабельных объектов в приложении обеспечить это в подавляющем большинстве случаев легко.

                                                                                        Или Вы просто хотите использовать иммутабельные объекты не как иммутабельные объекты сами по себе, а как костыль для обхода грубых логических ошибок где один и тот же объект используется для чтения и записи в функции которая не поддерживает in-place обработку?
                                                                                          0

                                                                                          Нет, я их хочу использовать для предотвращения подобных ошибок!

                                                                                            0
                                                                                            Вы логическую ошибку так просто банально маскируете.
                                                                                            Это костыль как он есть, assert(input != output) в такой функции на порядок полезнее.
                                                                                              0

                                                                                              Поясните, почему вы считаете что immutable просто маскирует ошибку.


                                                                                              А assert — это рантайм-проверка. Хотелось бы чтобы компилятор следил.

                                                                                                –2

                                                                                                Я бы посоветовал пытаться из С++ сделать D. Хотите использовать возможности на D, так и пишите на нём. Программистам C++ и так тошно от нагромождения шаблонов.

                                                                                                  0
                                                                                                  Потому что Ваша функция исходит из предположения что input != output. Это ее контракт. В момент когда Вы начинаете пользоваться функцией вне ее контракта Вы допускаете ошибку. Возьмите к примеру классификацию функций thread-safe, reentrant и не-reentrant. Если Вы пытаетесь использовать не-реентрабельную функцию из нескольких потоков, то что это будет? Ошибка. Вы нарушили контракт функции и огребете из-за этого проблемы. То же самое и здесь. Но вместо того чтобы проверить что этот контракт соблюдается и исправить соответствующие вызовы, Вы предлагаете каждый раз создавать копию input чтобы даже при нарушении контракта функция продолжала работать. А поскольку делать вручную это лень то Вы считаете хорошим решением обернуть это в хитрый шаблон который сделает эту копию автоматически. В итоге вызов нарушающий контракт не исправлен, а програма плодит без нужды копии объектов, хотя и работает. В моем понимании это «костыль».
                                                                                                    0

                                                                                                    Где, ну где вы вычитали что я предлагаю делать копию?! С копией как было бы очень просто — достаточно было бы убрать ссылочность у параметра.

                                                                                                      0
                                                                                                      Эм, а что Вы, простите, предлагаете еще делать с функцией из Вашего примера?
                                                                                                        0

                                                                                                        Передавать первым параметром ссылку на Immutable… где Immutable — нормальная реализация шаблона, а не как у автора.

                                                                                                          0
                                                                                                          Так это и есть создание копии. От того что она будет создана возможно не в месте непосредственного вызова а несколько раньше (там где создавался Immutable) ничего принципиально не меняется.
                                                                                                            0

                                                                                                            Можно же обойтись перемещением значения внутрь создаваемого immutable. Перемещение не такое затратное как копия.

                                                                                                              0
                                                                                                              Если у Вас есть функция f(in, out) и Вы вызываете f(a,a) то все сломается, в какие бы темплейты это не было завернуто. Работать оно может только в случае вызова f(copy_a, a), независимо от того как именно будет порождена copy_a. Причем если у Вас есть возможность сделать move в новый объект который будет дальше передаваться вместо исходного, то вместо

                                                                                                              immutable<T> b = move(a)
                                                                                                              

                                                                                                              гораздо практичнее «создать иммутабельный объект»

                                                                                                              const T& b = a;
                                                                                                              

                                                                                                              или в крайнем случае

                                                                                                              const T b = move(a);
                                                                                                              

                                                                                                              … и он будет работать во всех тех же ситуациях где работал бы исходный вариант с move. Минус темплейты, плюс невозможность скомпилировать пример с f(a,a).
                                                                                                                0

                                                                                                                Вы опять рассуждаете про внешний код! Забудьте про него, вы пишите только функцию. Как вы будете обеспечивать невозможность компиляции f(a, a)?

                                                                                                                  +1
                                                                                                                  Можно же обойтись перемещением значения внутрь создаваемого immutable

                                                                                                                  Это перемещение может сделать только внешний код

                                                                                                                  Как вы будете обеспечивать невозможность компиляции f(a, a)?

                                                                                                                  Да никак это на этапе компиляции в описанных исходных условиях невозможно сделать. Как собственно и в варианте с Immutable который скомпилируется и породит копию которая в большинстве случаев будет не нужна.

                                                                                                                  Как поступить правильно если у нас есть подобная функция? Я это уже написал. Либо мы считаем что in и out должны быть разными (это является частью контракта) и пишем assert(in != out) и тестированием проверяем что мы его не триггерим. Либо мы допускаем что in и out могут быть одинаковыми и закладываться на обратное мы не можем и переписываем f(a,b) так чтобы она правильно работала и в случае a==b. Один из возможных примеров подобного переписывания:

                                                                                                                  void foo(const X &a, X &b)
                                                                                                                  {
                                                                                                                    std::unique_ptr<X> local_a
                                                                                                                    if (&a == &b)
                                                                                                                      local_a.reset(new X(a));
                                                                                                                    const X& a_to_use = (&a == &b) ? *local_a : a;
                                                                                                                    // work with a_to_use
                                                                                                                  }
                                                                                                                  

                                                                                                                  Причем в отличие от темплейта этот пример породит копию только там где это действительно нужно. В чем плюс от шаблона-то?
                                                                                                                    0
                                                                                                                    Это перемещение может сделать только внешний код

                                                                                                                    И его сделает внешний код. В чем проблема-то?

                                                                                                                      0
                                                                                                                      Позволю себе процитировать Ваш ответ

                                                                                                                      Вы опять рассуждаете про внешний код! Забудьте про него, вы пишите только функцию


                                                                                                                      Может Вы перестанете ходить кругами? А то у Вас когда Вам хочется внешний код может сделать move, но когда я пишу Вам что в этом случае операцию move можно заменить на приведение к const то у Вас сразу оказывается что внешний код трогать нельзя. Определитесь уже.
                                                                                                                        0

                                                                                                                        Вы понимаете что такое "контракт функции"?


                                                                                                                        Вы предлагаете писать const во внешнем коде — но это никак не контролируется. Контракт функции позволяет вызвать ее, передав ей любую переменную. Хоть константную — хоть нет.


                                                                                                                        Я предлагаю ввести тип данных, который будет гарантировать неизменность. Во внешнем коде может использоваться копирование — а может использоваться перемещение, на выбор. Но независимо от того что выберет программист, который пишет внешний код — внутри функции параметр меняться не будет.

                                                                                                                          +1
                                                                                                                          Я Вам очень подробно расписал что делать с функцией не меняя условий его вызова.Это либо assert и ловим багу в коде который вызывает функцию, либо правим багу возникающую в функции для частного случая in==out. Ни один из этих вариантов не требует модификации ничего кроме кода функции, но вы предпочли их просто проигнорировать. Это общий случай и он гарантирует что все будет работать как надо. Есть еще частный, который вы приводите с move как обоснование того почему ваш код «может и не порождать лишней копии». Этот код требует определенных гарантий во внешнем коде и при наличии этих гарантий тривиально заменяется на использование иммутабельного объекта без нужды в шаблонах.

                                                                                                                          Давайте для простоты рассмотрим общий случай. Мой код не порождает копий, не требует шаблонов, не требует изменения сигнатуры функции, гарантированно работает всегда. Ваш требует шаблонов, порождает ненужные копии и требует ручной (и опасной в вашем понимании) оптимизации для того чтобы их избежать. Вроде бы очевидно какой из этих вариантов заведомо лучше, разве нет?
                                                                                                                            0

                                                                                                                            Какую опасную оптимизацию требует мой код?

                                                                                                                              0
                                                                                                                              Ручную расстановку move для потенциально расшаренных объектов.
                                                                                                                              Но в общем я понял что общаться в Вами бесполезно, Вы просто наугад выбираете одну из моих фраз и пишете по ней какие-то общие слова игнорируя контекст. Вы не видите ничего плохого в том чтобы жить с кодом который (одно из двух) либо вызывает функцию на запись в неожиданный объект, либо не делает копии там где это нужно, а я вижу, и не считаю что вариант «раз где-то без копирования все может сломаться, то будем по умолчанию делать копию везде, но с помощью этого костыля и ручной расстановки move по коду часть этих копий уберем» является оправданным решением этой проблемы. Давайте закончим на этом.
                                                                                                            0

                                                                                                            Приведите, пожалуйста пример нормальной реализации шаблона Immutable.
                                                                                                            Хотя бы для значений… \
                                                                                                            И не обязательно по букве и духу ФП. Хотя желательно, чтобы с "завернутым" объектом можно было работать как с оригинальным.
                                                                                                            Заранее благодарен.

                                                                                                              +1
                                                                                                              template<typename T> class immutable {
                                                                                                              private: 
                                                                                                                  T const value;
                                                                                                              public:
                                                                                                                  immutable(immutable const &) = delete;
                                                                                                                  void operator = (immutable const &) = delete;
                                                                                                              
                                                                                                                  template<typename... Args> immutable(Args&&... args) 
                                                                                                                      : value(std::forward<Args>(args)...) { }
                                                                                                              
                                                                                                                  T const & operator () () const {
                                                                                                                       return value;
                                                                                                                  }
                                                                                                              
                                                                                                                  T const * operator -> () const {
                                                                                                                       return &value;
                                                                                                                  }
                                                                                                              }

                                                                                                              Если я ничего не напутал — этого должно быть достаточно.


                                                                                                              Особые специализации не нужны — всегда можно добавить круглые скобки и получить ссылку на базовый объект.


                                                                                                              Рекурсивная иммутабельность тоже не нужна — всегда можно расставить вложенные immutable вручную.

                                                                                                                0

                                                                                                                Вот это мне уже нравится. А особая специализация нужна для T*: чтобы была возможность в конструкторе не указывать new и вызывать методы не как ()->, а сразу ->:


                                                                                                                template<typename T> class immutable<T*> {
                                                                                                                private: 
                                                                                                                    std::unique_ptr<T> const value;
                                                                                                                public:
                                                                                                                    immutable(immutable const &) = delete;
                                                                                                                    void operator = (immutable const &) = delete;
                                                                                                                
                                                                                                                    template<typename... Args> immutable(Args&&... args) 
                                                                                                                        : value(new T(std::forward<Args>(args)...)) { }
                                                                                                                
                                                                                                                    T const * operator () () const {
                                                                                                                         return value.get();
                                                                                                                    }
                                                                                                                
                                                                                                                    T const * operator -> () const {
                                                                                                                         return value.get();
                                                                                                                    }
                                                                                                                }
                                                                                                                  0

                                                                                                                  У вас в вашем иммутабельном указателе таки появилась рекурсивная иммутабельность, что является усложнением.


                                                                                                                  Для простых указателей вообще нет смысла использовать иммутабельную обертку, проще указатель копировать.

                                                                                                                    0
                                                                                                                    У вас в вашем иммутабельном указателе таки появилась рекурсивная иммутабельность, что является усложнением.

                                                                                                                    А разве это не является целью? Или под иммутабельностью понимается исключительно защита поля (указателя), а не защита содержимого, на который он указывает?


                                                                                                                    Если только первое, тогда иммутабельность при работе с указателями абсолютно бессмыслена.

                                                                                                                      0

                                                                                                                      Защитить содержимое довольно просто: immutable<T>*

                                                                                                                        0

                                                                                                                        А защитить и содержимое, и поле? На вариант immutable<immutable<T>*> не очень приятно смотреть. Хотя да, действительно, указатель-то передётся по значению обычно.

                                                                                                                          0

                                                                                                                          Вот именно, указатель — по значению.


                                                                                                                          Кстати, не вижу проблем с двойной защитой. Почему-то же никого не смущают конструкции вида char const * const * const. Так почему вложенные immutable — это плохо? Если из-за размера, то можно и сократить...

                                                                                                                    0

                                                                                                                    Вы забыли про оператор разыменования *

                                                                                                                      0

                                                                                                                      Точно, спасибо.

                                                                                                                    –1

                                                                                                                    похоже на std::reference_wrapper…
                                                                                                                    у Вас появилось копирование, против которого уже негативно высказывались

                                                                                                                      0

                                                                                                                      Где копирование?

                                                                                                                        0

                                                                                                                        спасибо за минус
                                                                                                                        К https://habrahabr.ru/post/322208/#comment_10085776
                                                                                                                        Копирование появилось в шаблонном конструкторе: инициализирующее значение, копируется или перемещается в value.

                                                                                                                          0
                                                                                                                          Копирование появилось в шаблонном конструкторе: инициализирующее значение копируется или перемещается в value.

                                                                                                                          Неверно. Всё, что делается в конструкторе — это perfect forwarding аргументов в вызов соотвествующего конструктора value.

                                                                                                                            0

                                                                                                                            std::forward приводит свой аргумент к rvalue только тогда, когда этот аргумент связан с rvalue. В этом случае value буден сконструировано через вызов конструктора перемещения.
                                                                                                                            В противном случае value будет сконструировано через конструктор копии.


                                                                                                                            Если Вы мне не верите, то предлагаю убедиться: С. Майерс "Современный и эффективный С++", главы 5 и 8

                                                                                                                              +1

                                                                                                                              А что неправильно-то?


                                                                                                                              immutable<Foo> f(x); // копируем
                                                                                                                              immutable<Foo> f(std::move(x)); // перемещаем
                                                                                                                              immutable<Foo> f(getFoo()); // тоже перемещаем
                                                                                                                              immutable<Foo> f(1, 2, 3, 4, 5); // создаем на месте

                                                                                                                              Один шаблонный конструктор покрывает сразу все ситуации...

                                                                                                                                0

                                                                                                                                Вот пример:


                                                                                                                                #include <QDebug>
                                                                                                                                
                                                                                                                                class Test {
                                                                                                                                public:
                                                                                                                                    Test()
                                                                                                                                    {
                                                                                                                                        qDebug() << __PRETTY_FUNCTION__;
                                                                                                                                    }
                                                                                                                                
                                                                                                                                    Test(const Test &)
                                                                                                                                    {
                                                                                                                                        qDebug() << __PRETTY_FUNCTION__;
                                                                                                                                    }
                                                                                                                                
                                                                                                                                    Test(Test &&)
                                                                                                                                    {
                                                                                                                                        qDebug() << __PRETTY_FUNCTION__;
                                                                                                                                    }
                                                                                                                                private:
                                                                                                                                };
                                                                                                                                
                                                                                                                                template<typename T> class immutable
                                                                                                                                {
                                                                                                                                private:
                                                                                                                                    T const value;
                                                                                                                                public:
                                                                                                                                    immutable(immutable const &) = delete;
                                                                                                                                    void operator = (immutable const &) = delete;
                                                                                                                                
                                                                                                                                    template<typename... Args> immutable(Args&&... args)
                                                                                                                                        : value(std::forward<Args>(args)...)
                                                                                                                                    {
                                                                                                                                        qDebug() << __PRETTY_FUNCTION__;
                                                                                                                                    }
                                                                                                                                
                                                                                                                                    T const & operator () () const {
                                                                                                                                         return value;
                                                                                                                                    }
                                                                                                                                
                                                                                                                                    T const * operator -> () const {
                                                                                                                                         return &value;
                                                                                                                                    }
                                                                                                                                };
                                                                                                                                
                                                                                                                                int main(int argc, char *argv[])
                                                                                                                                {
                                                                                                                                    Test t1;
                                                                                                                                    Test t2;
                                                                                                                                
                                                                                                                                    immutable<Test> a(t1);
                                                                                                                                    immutable<Test> b(std::move(t2));
                                                                                                                                
                                                                                                                                    return 0;
                                                                                                                                }

                                                                                                                                Результат:
                                                                                                                                Test::Test()
                                                                                                                                Test::Test()
                                                                                                                                Test::Test(const Test&)
                                                                                                                                immutable::immutable(Args&& ...) [with Args = {Test&}; T = Test]
                                                                                                                                Test::Test(Test&&)
                                                                                                                                immutable::immutable(Args&& ...) [with Args = {Test}; T = Test]

                                                                                                                                  +1

                                                                                                                                  Ну да, все ожидаемо. a(t1) копирует, а через std::move — не копирует. А вам как надо?

                                                                                                                                    0

                                                                                                                                    С вопросом где копирование мы разобрались.
                                                                                                                                    Я про то, что так или иначе копию делать придется, а за лишние копирование, среди комментов было высказано немало неласковых слов.

                                                                                                                                      +1

                                                                                                                                      Во-первых, есть огромная разница между однократным копированием и копированием при каждом обращении.


                                                                                                                                      Во-вторых, есть варианты полностью избежать копирования: перемещение и создание объекта на месте через конструктор.

                                                                                                                                        –1

                                                                                                                                        приведите примеры использования двух подходов, если несложно

                                                                                                                                        0

                                                                                                                                        Зачем делать копию, когда можно сконструировать объект на месте?


                                                                                                                                        class Test {
                                                                                                                                        public:
                                                                                                                                            Test(int a, int b)
                                                                                                                                        ...
                                                                                                                                        immutable<Test> c(1, 2)
                                                                                                                                        
                                                                                                                                      0

                                                                                                                                      И еще: в С++ есть тонкость:


                                                                                                                                      • шаблонный конструктор НЕ замещает стандартный копирующий конструктор. При точном соответствии типов будет вызван обычный копирующий конструктор.
                                                                                                                                      • шаблонный конструктор никогда не используется для генерации обычного конструктора копий, при его отсутствии, он будет сгенерирован неявно (если только явно не запрещен).
                                                                                                                                        Если память не изменяет, Страуструп, "Язык программирования С++" 4 издание, глава 13, первый пункт из стандарта (тоже на память)
                                                                                                                                        0

                                                                                                                                        Зачем вы пересказываете азы языка?

                                                                                                                                          0
                                                                                                                                          шаблонный конструктор НЕ замещает стандартный копирующий конструктор.

                                                                                                                                          И поэтому копирующий конструктор дополнительно помещается как удалённый, потому что возможность копирования иммутабельных данных нам не интересна


                                                                                                                                          immutable(immutable const &) = delete;
                                                                                                            +1

                                                                                                            restrict не поможет… Это подсказка компилятору для генерации более эффективного когда. Если программист нарушает, то это неопределенное поведение. Максимум, что может сделать компилятор — это выдать предупреждение, и то в тривиальном случае.


                                                                                                            N1570 ISO/IEC 9899:201x

                                                                                                              0

                                                                                                              Ну тем более...

                                                                                                  0
                                                                                                  Проблема иммутабельных данных (да и вообще ФП) в с++ очень проста: базовые концепции ФП, его краеугольные камни, очень красиво выглядят на бумаге, но их «честная» реализация накладывает ощутимый оверхед, причем на любые операции. С++ — не тот язык, на котором решают задачи, где такое допустимо.
                                                                0
                                                                (не в ту ветку)
                                                                  +3
                                                                  В С++17 вместо return Immutable(a.value() + b); можно записать return Immutable(a.value() + b);

                                                                  Я похоже невнимальный, но в двух выделенных участках я не вижу различий.

                                                                    0

                                                                    Спасибо, пофиксил. Ставлю +

                                                                    0
                                                                    Не могли бы вы рассказать, зачем здесь m_value?

                                                                    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 Base value() const
                                                                        {
                                                                            return m_value;
                                                                        }
                                                                    


                                                                    Мне кажется, что так было бы и проще, и на одну копию меньше:
                                                                    template <typename Base>
                                                                    class immutable_value<Base, true> : private Base
                                                                    {
                                                                    public:
                                                                        using value_type = Base;
                                                                        constexpr explicit immutable_value(const Base &value)
                                                                            : Base(value)
                                                                        {
                                                                        }
                                                                    
                                                                        // ...
                                                                        constexpr Base value() const
                                                                        {
                                                                            return static_cast<Base>(*this);
                                                                        }
                                                                    

                                                                      0

                                                                      Только все-таки *static_cast<Base const*>(this).

                                                                        0
                                                                        Если причесать, то можно так:
                                                                        return *this;
                                                                        

                                                                    Only users with full accounts can post comments. Log in, please.