LINUX.ORG.RU

С++ и правило нуля

 


0

1

Возник такой вопрос.

Есть в C++ правило нуля.

По этому правилу для владения ресурсами вместо простых указателей нужно использовать специальные классы-обёртки, такие как std::unique_ptr и std::shared_ptr и не определять конструкторы перемещения/копирования и такие же операторы присваивания.

Но если я объявляю деструктор класса - то обязан объявить и выше названые конструкторы с операторами присваивания (например, в виде = default).

Есть ли какой-то способ упростить вот этот код?

Что бы не писать каждый раз определения с = default.

#include <iostream>
#include <memory>

struct S
{
    S(void)
    {
        std::cout << "Constructor S" << std::endl;
    }

    S(const S &) = default;
    S(S &&) = default;
    S &operator=(const S &) = default;
    S &operator=(S &&) = default;

    virtual ~S(void)
    {
        std::cout << "Destructor S" << std::endl;
    }
};

int main(void)
{
    auto s = std::make_shared<S>();
    return EXIT_SUCCESS;
}

вместо простых указателей нужно использовать специальные классы-обёртки, такие как std::unique_ptr и std::shared_ptr и _не_ определять конструкторы перемещения/копирования

А если определить, что будет?

если я объявляю деструктор класса - то обязан объявить и выше названые конструкторы

Если память не изменяет, пока что не обязан

anonymous ()

Есть ли какой-то способ упростить вот этот код?

Без макросов, вроде, нет. Можно опускать это:

    S(const S &) = default;
    S &operator=(const S &) = default;

Но их генерация при наличии деструкторов кажется уже deprecated, так что может и не стоит их опускать.

xaizek ★★★★★ ()
Ответ на: комментарий от rumgot

Потому что при отсутствии сырых указателей или других не-RAII сущностей компилятор сам сгенерирует код, в котором всё нормально с управлением ресурсами.

xaizek ★★★★★ ()
Ответ на: комментарий от yoghurt

boost::shared_ptr, по крайней мере, именно так и делает.

This constructor is a template in order to remember the actual pointer type passed. The destructor will call delete with the same pointer, complete with its original type, even when T does not have a virtual destructor, or is void.

https://www.boost.org/doc/libs/1_70_0/libs/smart_ptr/doc/html/smart_ptr.html#...

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

3) Uses the delete-expression delete ptr if T is not an array type; delete[] ptr if T is an array type (since C++17) as the deleter. Y must be a complete type. The delete expression must be well-formed, have well-defined behavior and not throw any exceptions. This constructor additionally does not participate in overload resolution if the delete expression is not well-formed.

https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr

Makhno ()
Ответ на: комментарий от Makhno

Конструктор же создаёт delete ptr для типа T. Если потом создать другой std::shared_ptr<U>, где U наследуется от T, то deleter не поменяется. Поэтому следующий код выведет

~B
~A
#include <iostream>
#include <memory>

struct A
{
    ~A() { std::cout << "~A\n"; }
};

struct B : A
{
    ~B() { std::cout << "~B\n"; }
};

int
main()
{
    std::shared_ptr<A> a = std::shared_ptr<B>(new B);
}

xaizek ★★★★★ ()
Ответ на: комментарий от Makhno

Бустовые указатели могут делать что угодно, речь шла про стандартные.

std::unique_ptr<Base> b;
b.reset(new Dervied);

Внимание, вопрос - откуда unique_ptr-у (специализированному на Base) узнать, что указатель, переданный в reset - это на самом деле указатель на неведомый ему Derived?

yoghurt ★★★★★ ()
Ответ на: комментарий от yoghurt

Внимание, вопрос - откуда unique_ptr-у (специализированному на Base) узнать, что указатель, переданный в reset - это на самом деле указатель на неведомый ему Derived?

Это без проблем реализуется. reset может переопределять делитер.

tsaruchetkav1 ()
Ответ на: комментарий от xaizek

Ну вот если в классе есть члены-указатели. То полюбому нужно фигачить специальные функции члены (конструкторы копирования/перемещения и операторы присваивания с копированием/переносом). Верно?

rumgot ★★★★★ ()
Ответ на: комментарий от tsaruchetkav1

C++11:

The implicit definition of a copy constructor as defaulted is deprecated if
the class has a user-declared copy assignment operator or a user-declared
destructor. The implicit definition of a copy assignment operator as
defaulted is deprecated if the class has a user-declared copy constructor or a
user-declared destructor ([class.dtor], [class.copy]). In a future revision of
this International Standard, these implicit definitions could become deleted
([dcl.fct.def]).
xaizek ★★★★★ ()
Ответ на: комментарий от tsaruchetkav1
std::unique_ptr<Base> b;
Base *ptr = new Derived;
b.reset(ptr);

Вперед, реализовывай.

Бустовкий API-контракт несколько другой:

      template<class Y> void reset(Y * p);
      template<class Y, class D> void reset(Y * p, D d);
      template<class Y, class D, class A> void reset(Y * p, D d, A a);

и в такой форме действительно можно «обновить» делитер до делитера правильного типа, но в std это видимо завозят не раньше c++17.

yoghurt ★★★★★ ()

По этому правилу для владения ресурсами вместо простых указателей нужно использовать специальные классы-обёртки, такие как std::unique_ptr и std::shared_ptr и не определять конструкторы перемещения/копирования и такие же операторы присваивания.

Но если я объявляю деструктор класса - то обязан объявить и выше названые конструкторы с операторами присваивания (например, в виде = default).

Если тебе нужен move-only тип, то конструктор копирования и оператор присваивания надо объявлять с = delete, а не = default.

annulen ★★★★★ ()
Ответ на: комментарий от anonymous

Имел ввиду именно правило нуля.

Но вот полный пример:

#include <iostream>
#include <memory>

struct S
{
    S(void): handle(0)
    {
        std::cout << "Constructor S" << std::endl;
    }

    S(const S &) = default;
    S(S &&) = default;
    S &operator=(const S &) = default;
    S &operator=(S &&) = default;

    virtual ~S(void)
    {
        std::cout << "Destructor S" << std::endl;
    }

    int get(void) const
    {
        return handle;
    }

    private:
        int handle;
};

class C
{
    public:
        C(void): m_s(std::make_shared<S>())
        {
            std::cout << "Constructor C" << std::endl;
        }

        S get(void) const
        {
            return *m_s;
        }

    private:
        std::shared_ptr<S> m_s;
};

template <typename T = C>
void action(T&& c)
{
    auto s = std::forward<S>(c.get());
    std::cout << "Handle is " << s.get() << std::endl;
}

int main(void)
{
    C c;
    action(c);
    S s;
    s = c.get();

    return EXIT_SUCCESS;
}

И тут class C следует правилу нуля, а struct S нет. Из-за объявленного деструктора на struct S действует правило пяти.

А хотелось бы и struct S правилу нуля подчинить.

Поэтому пробовал вот так:

struct S
{
    S(void)
    {
        std::cout << "Constructor S" << std::endl;
    }
};

auto makeS(void)
{
    auto deleter = [] (S *a)
    {
        std::cout << "Deleter S" << std::endl;
        delete a;
    };
    std::unique_ptr<S, decltype(deleter)> u(nullptr, deleter);
    u.reset(new S);
    return u;
}

И всё было бы хорошо, но, например, в таком варианте для S s; некому будет ресурсы освобождать, что не хорошо.

Поэтому была идея как-то упростить декларацию struct S с деструктором.

Конечно можно не писать все эти:

    S(const S &) = default;
    S(S &&) = default;
    S &operator=(const S &) = default;
    S &operator=(S &&) = default;

Но тогда, как минимум, семантика перемещения работать не будет.

user0xff ()