Получение некопируемого объекта из попытки

У меня есть следующий класс:

class ShaderProgram {
    private:
        GLuint shaderProgram;
        std::string getCompilationError(GLuint shader);
        std::string getLinkingError(GLuint shaderProgram);

    public:
        // ! Might throw exception !
        ShaderProgram(const std::string &vertexShaderPath, const std::string &fragmentShaderPath);
        ~ShaderProgram();
        // Delete the copy constructor and assignment operator, you should not copy this class
        ShaderProgram(const ShaderProgram&) = delete;
        ShaderProgram* operator=(const ShaderProgram&) = delete;
        // We do allow move constructor and move assignment
        ShaderProgram(ShaderProgram&& other);
        ShaderProgram& operator=(ShaderProgram&&);
};

Вам не разрешено копировать и экземпляр этого класса из-за GLuint shaderProgram: его нужно удалить ровно один раз, поэтому, если кто-то делает копии объекта ShaderProgram и одна из копий разрушается, все они становятся непригодными для использования.

Теперь конструктор может бросить. Например: если файл не найден или возникли ошибки при компиляции/линковке gsl. Я хочу поймать эти ошибки, вывести красивое сообщение об ошибке и выйти из программы. Если ошибки нет, я могу продолжить, но я застрял, вытаскивая объект из области попытки. Единственное решение, которое я вижу, это использовать указатели:

ShaderProgram* program;

try {
    program = new ShaderProgram("PATH", "PATH");

} catch (Exception e) {
    // print error
    // Exit the program
}
// code
delete program;

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

Другая возможность — просто вставить весь код, использующий ShaderProgram, внутрь try. ShaderProgram живет очень долго, и поэтому это привело бы к очень большому блоку try. Это может затруднить чтение кода.

Я чувствую, что должно быть более правильное решение. Каким будет это решение?


person Cheiron    schedule 07.12.2020    source источник
comment
Я не вижу проблемы. Разве весь код, использующий созданный экземпляр, не находится также внутри блока try? Я имею в виду, что если у вас есть исключение, то нет объекта, который вы могли бы использовать после блока try.   -  person 463035818_is_not_a_number    schedule 07.12.2020
comment
Этот объект очень долгоживущий, так что большая часть кода попадет в блокировку try. Это, конечно, возможно, но код становится труднее читать.   -  person Cheiron    schedule 07.12.2020
comment
Я бы сказал, что вы пытаетесь поймать исключение не в том месте. Как я уже говорил, весь код, который идет после try, не может использовать объект, если он не был успешно создан.   -  person 463035818_is_not_a_number    schedule 07.12.2020
comment
Это действительно так, ловушка напечатает ошибку и выйдет из программы: мы не можем исправить эту ошибку.   -  person Cheiron    schedule 07.12.2020
comment
хорошо, пожалуйста, добавьте это к вопросу. Это меняет ситуацию. обращаться с ними красиво, как правило, не завершает приложение;)   -  person 463035818_is_not_a_number    schedule 07.12.2020


Ответы (3)


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

#include<iostream>
#include<cmath>

struct ShaderProgram {
    ShaderProgram(const ShaderProgram&) = delete;
    ShaderProgram() {
        throw 1;
    }
};

int main(){


    ShaderProgram x = [](){
        try {
            return ShaderProgram{};
        } catch (...) {
            std::cout << "exception occured" << std::endl;
            std::terminate();
        }
    }();
}

Текущая демонстрация

person 463035818_is_not_a_number    schedule 07.12.2020
comment
Вопрос, поскольку я не могу полностью уточнить это на данный момент: это может не компилироваться для компиляторов до С++ 17 для конкретного сомнительного класса, поскольку на самом деле это инициализация копирования, а исключение копирования может не применяться здесь в сомнении? - person Secundi; 07.12.2020
comment
@Secundi да, до C++17 удаление копии было только оптимизацией, т. е. объект все еще должен был быть копируемым, см. здесь: godbolt.org/z/WqGxvq - person 463035818_is_not_a_number; 07.12.2020
comment
Мне не нравится использовать прерывание вместо возврата, так как вывод терминала становится более пугающим. Но он работает и делает именно то, что я хочу. - person Cheiron; 07.12.2020
comment
@Cherion на самом деле std::exit может быть более подходящим. Я просто подумал, что когда вы завершаете работу из-за исключения, это может быть std::terminate - person 463035818_is_not_a_number; 07.12.2020

Почему вы не используете try catch внутри самого конструктора? Блоки try catch следует применять как можно ближе к критическим точкам. Кроме того, ссылаться на, возможно, не полностью построенный this/objects, как правило, не очень хорошая идея и в большинстве случаев, по крайней мере, неопределенное поведение.

Обновление, так как вы разместили дополнительную информацию:

Со схемой unique_ptr + factory:

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

Решение без unique_ptrs:

Отложите критические части вашего конструктора на дополнительный приватный метод и примените там try catch. Затем отслеживайте правильность построения просто через неисправный бит/элемент. Это гарантирует достоверность вашего объекта на языковом уровне вашей проблемы, но имеет некоторые недостатки, поскольку пользователи могут забыть проверить состояние в зависимости от контекста использования. Некоторые части стандартных io-библиотек используют этот шаблон.

В любом случае, я предпочитаю решение unique_ptr.

person Secundi    schedule 07.12.2020
comment
Итак, как мне сообщить конструктору объекта, что создание объекта не удалось и объект не должен использоваться? - person Cheiron; 07.12.2020
comment
Полностью запретите публичное строительство таким образом, используйте сомнительный фабричный шаблон, который может предоставить эту информацию для внешнего мира. Поскольку вы не разрешаете копирование, вы можете просто указать unique_ptrs, как вы уже упоминали. Это также прояснит ситуацию для пользователей вашего класса. - person Secundi; 07.12.2020

Один из методов заключается в использовании общедоступной функции «инициализации» с аргументами, которые в настоящее время передаются конструктору. Создайте пустую «ShaderProgram» без аргументов за пределами блока try-catch и вызовите для нее «init» внутри блока с аргументами.

public:
    ShaderProgram();
    ~ShaderProgram();
    // ! Might throw exception !
    int init(const std::string &vertexShaderPath, const std::string &fragmentShaderPath);
person TKA    schedule 07.12.2020
comment
Но это не соответствует принципу RAII, который мне очень нравится. Вы правы, что это работает. - person Cheiron; 07.12.2020