0

Использование `destructor = delete;` в C++

1

Рассмотрим следующий класс:

struct S { ~S() = delete; };

Кратко, суть вопроса состоит в том, что я не могу создавать экземпляры S, например, используя S s{};, потому что не могу их уничтожить. Как упоминалось в комментариях, я все же могу создать экземпляр, вызвав S *s = new S;, но не смогу его удалить. Поэтому единственное применение, которое я вижу для удаленного деструктора, это что-то вроде:

struct S {
    ~S() = delete;
    static void f() { }
};

int main() {
    S::f();
}

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

Какие другие применения (если такие есть) могут иметь удаленные деструкторы?

5 ответ(ов)

0

Если у вас есть объект, который никогда не должен быть уничтожен с помощью delete, находиться в памяти на стеке (автоматическое хранение) или быть частью другого объекта, использование =delete поможет предотвратить все эти случаи.

struct Handle {
  ~Handle()=delete;
};

struct Data {
  std::array<char, 1024> buffer;
};

struct Bundle: Handle {
  Data data;
};

using bundle_storage = std::aligned_storage_t<sizeof(Bundle), alignof(Bundle)>;

std::size_t bundle_count = 0;
std::array<bundle_storage, 1000> global_bundles;

Handle* get_bundle() {
  return new ((void*)global_bundles[bundle_count++]) Bundle();
}

void return_bundle(Handle* h) {
  assert(h == (void*)global_bundles[bundle_count - 1]);
  --bundle_count;
}

char get_char(Handle const* h, std::size_t i) {
  return static_cast<Bundle*>(h)->data.buffer[i];
}

void set_char(Handle const* h, std::size_t i, char c) {
  static_cast<Bundle*>(h)->data.buffer[i] = c;
}

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

Я считаю, что ничего из вышесказанного не ведет к неопределённому поведению; не уничтожение Bundle приемлемо, так же как и создание нового объекта на его месте.

Интерфейс не обязательно должен раскрывать, как именно работает Bundle. Он представляется как непрозрачный Handle.

Такая техника может быть полезна, если другим частям кода нужно знать, что все Handle находятся в конкретном буфере или их жизненный цикл отслеживается определённым образом. Возможно, это также могло бы быть реализовано с помощью приватных конструкторов и функций-фабрик-друзей.

0

Ваша идея о запрете неправильной деалокации в C++ действительно интересна. Рассмотрим два примера использования этого подхода.

Первый пример показывает, как можно предотвратить ошибочную деалокацию с помощью удаления деструктора:

#include <stdlib.h>

struct S {
    ~S() = delete; // Деструктор недоступен
};

int main() {
    S* obj = (S*) malloc(sizeof(S)); // Выделение памяти

    // корректное освобождение памяти
    free(obj);

    // ошибка: нельзя вызывать delete на obj, так как деструктор удален
    // delete obj; // Это приведет к ошибке компиляции

    return 0;
}

В этом случае, если кто-то попытается использовать delete для объекта obj, возникнет ошибка компиляции, что предотвращает неправильное использование оператора.

Второй пример демонстрирует более сложный подход, который является более "стилистическим" для C++:

#include <vector>

struct data {
    // Реализация структуры данных
};

struct data_protected {
    ~data_protected() = delete; // Деструктор недоступен
    data d;
};

struct data_factory {
    ~data_factory() {
        for (data* d : data_container) {
            // Это безопасно, потому что никто не может вызвать 'delete' на d
            delete d;
        }
    }

    data_protected* createData() {
        data* d = new data(); // Создаем объект data
        data_container.push_back(d); // Добавляем в контейнер
        return (data_protected*)d; // Возвращаем указатель как data_protected
    }

    std::vector<data*> data_container; // Контейнер для хранения данных
};

В этом примере data_protected запрещает вызов деструктора, тем самым гарантируя, что никто не сможет случайно удалить объект data с использованием delete. Вместо этого объекты data управляются через data_factory, что позволяет централизованно контролировать их жизненный цикл.

Эти подходы особенно полезны в случаях, когда у вас есть специальные механизмы распределения и освобождения памяти, и вы хотите избежать ошибок, связанных с неуместным использованием операторов new и delete.

0

Зачем помечать деструктор как delete?

Чтобы предотвратить его вызов, конечно же 😉

Какие есть варианты использования?

Я вижу как минимум три различных применения:

  1. Класс никогда не должен быть instantiated (создан); в этом случае я бы также ожидал удалённый конструктор по умолчанию.
  2. Экземпляр этого класса должен быть утечен; например, экземпляр синглтона для логирования.
  3. Экземпляр этого класса может быть создан и уничтожен только определённым механизмом; это может иметь место, например, при использовании FFI (Foreign Function Interface).

Чтобы проиллюстрировать последний пункт, представьте себе C-интерфейс:

struct Handle { /**/ };

Handle* xyz_create();
void xyz_dispose(Handle*);

В C++ вы бы хотели обернуть это в unique_ptr, чтобы автоматизировать освобождение, но что, если вы случайно напишете: unique_ptr<Handle>? Это будет катастрофа во время выполнения!

Поэтому вместо этого вы можете изменить определение класса:

struct Handle { /**/ ~Handle() = delete; };

И тогда компилятор не позволит вам использовать unique_ptr<Handle>, заставляя использовать правильный вариант unique_ptr<Handle, xyz_dispose> вместо этого.

0

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

Альтернативно (и даже более странно) вы могли бы выделить буфер, создать объект внутри него, а затем удалить буфер, чтобы восстановить место, но никогда не пытаться вызвать деструктор.

#include <iostream>

struct S { 
    const char* mx;

    const char* getx(){return mx;}

    S(const char* px) : mx(px) {}
    ~S() = delete; 
};

int main() {
    char *buffer = new char[sizeof(S)];
    S *s = new(buffer) S("не удаляем это..."); // Конструируем объект типа S в буфере.
    // Код, использующий s...
    std::cout << s->getx() << std::endl;

    delete[] buffer; // Освобождаем память без вызова деструктора...
    return 0;
}

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

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

Позволить программе выйти из main() и позволить окружению "очистить" память — это приемлемая техника, но к ней лучше прибегать только в случае строгой необходимости. В лучшем случае это отличный способ скрыть настоящие утечки памяти!

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

Я был бы рад увидеть реальное практическое применение этой способности.

Существует понятие статического класса (без конструкторов), и логически он не требует деструктора. Однако такие классы лучше реализовать как namespace и у них нет (хорошего) места в современном C++, если только они не являются шаблонными.

0

Создание экземпляра объекта с помощью new и отсутствие его удаления — это самый безопасный способ реализации Singleton в C++, так как он позволяет избежать проблем с порядком разрушения объектов. Типичный пример такой проблемы — это Singleton для логирования, который может быть доступен в деструкторе другого класса Singleton. Александреску посвятил целую секцию в своей классической книге "Современный C++ дизайн" способам решения проблем с порядком разрушения в реализации Singletone.

Деструктор, который удалён, является хорошим решением, поскольку даже сам класс Singleton не сможет случайно удалить экземпляр. Это также предотвращает странные случаи использования, такие как delete &SingletonClass::Instance() (если Instance() возвращает ссылку, как и должно; нет смысла возвращать указатель).

Тем не менее, в конечном итоге, всё это не так уж и примечательно. И, конечно, в первую очередь не стоит использовать Singleton'ы.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь