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

У меня есть класс Exception, для которого я хочу установить больше информации, прежде чем я его выброшу. Могу ли я создать объект Exception, вызвать некоторые из его функций, а затем выбросить его без создания каких-либо копий?

Единственный метод, который я нашел, - это указать указатель на объект:

class Exception : public std::runtime_error
{
public:
    Exception(const std::string& msg) : std::runtime_error(msg) {}
    void set_line(int line) {line_ = line;}
    int get_line() const {return line_;}
private:
    int line_ = 0;
};

std::unique_ptr<Exception> e(new Exception("message"));
e->set_line(__LINE__);
throw e;
...
catch (std::unique_ptr<Exception>& e) {...}

Но генерации исключений по указателю обычно избегают, так что есть ли другой способ?

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

throw Exception("message"); // or:
throw Exception("message", __LINE__); // or:
throw Exception("message", __FILE__); // or:
throw Exception("message", __LINE__, __FILE__); // etc.

person Claudiu    schedule 03.04.2017    source источник
comment
Добавление полей через вызовы функций может быстро стать немасштабируемым. Итак, ИМХО, добавление их через конструктор - лучшая практика.   -  person Jonas    schedule 03.04.2017
comment
@Jonas Я имел в виду, что он может стать немасштабируемым, если вы хотите иметь точный контроль над тем, какие параметры вы хотите установить. Я обновлю вопрос.   -  person Claudiu    schedule 03.04.2017
comment
Да, это может быть проблематично. Это одна из причин, по которой именованные параметры были бы хорошей функцией.   -  person Jonas    schedule 03.04.2017
comment
Не нужно создавать e в качестве указателя. В ваших 4 примерах бросков используются параметры по умолчанию, поэтому вам нужно определить только 1 конструктор.   -  person Richard Critten    schedule 03.04.2017
comment
@RichardCritten Хорошее наблюдение, действительно помогут параметры по умолчанию. Однако я вижу два возможных недостатка: 1. если значение, которое вас интересует, является последним параметром, вам нужно будет вручную заполнить все остальные параметры значениями по умолчанию; 2. если есть много параметров, вы получите конструкторы, похожие на функцию CreateWindow [bleh]   -  person Claudiu    schedule 03.04.2017
comment
Просто упомянем идею: иерархия классов (например, std::exception) может подойти вместо универсального класса Exception, который пытается инкапсулировать все.   -  person Andre Kostur    schedule 03.04.2017
comment
Решает ли Boost.Exception вашу настоящую проблему (добавляя множество разных вещей в исключение)?   -  person Jeffrey Bosboom    schedule 03.04.2017
comment
@JeffreyBosboom К сожалению, я не могу использовать boost в своем проекте, но это может быть хорошим ресурсом для тех, кто может. Использование множественного наследования для создания новых типов исключений — определенно интересная идея.   -  person Claudiu    schedule 04.04.2017


Ответы (3)


Как насчет использования std::move?

Exception e("message");
e.set_line(__LINE__);
throw std::move(e);

В качестве альтернативы вы можете создать сборщик в стиле Java следующим образом:

class ExceptionBuilder;

class Exception : public std::runtime_error
{
public:
    static ExceptionBuilder create(const std::string &msg);

    int get_line() const {return line_;}
    const std::string& get_file() const { return file_; }
private:
    // Constructor is private so that the builder must be used.
    Exception(const std::string& msg) : std::runtime_error(msg) {}

    int line_ = 0;
    std::string file_;

    // Give builder class access to the exception internals.
    friend class ExceptionBuilder;
};

// Exception builder.
class ExceptionBuilder
{
public:
    ExceptionBuilder& with_line(const int line) { e_.line_ = line; return *this; }
    ExceptionBuilder& with_file(const std::string &file) { e_.file_ = file; return *this; }
    Exception finalize() { return std::move(e_); }
private:
    // Make constructors private so that ExceptionBuilder cannot be instantiated by the user.
    ExceptionBuilder(const std::string& msg) : e_(msg) { }
    ExceptionBuilder(const ExceptionBuilder &) = default;
    ExceptionBuilder(ExceptionBuilder &&) = default;

    // Exception class can create ExceptionBuilders.
    friend class Exception;

    Exception e_;
};

inline ExceptionBuilder Exception::create(const std::string &msg)
{
    return ExceptionBuilder(msg);
}

Используется следующим образом:

throw Exception::create("TEST")
    .with_line(__LINE__)
    .with_file(__FILE__)
    .finalize();
person Karl Nicoll    schedule 05.04.2017
comment
Что касается подхода std::move - все равно создаются две копии исключения. В книге Мейерса «Более эффективный C++» он говорит: C++ указывает, что объект, созданный как исключение, всегда копируется, так что я думаю, что это может быть и здесь. - person Claudiu; 06.04.2017
comment
Мне нравится второй подход — это очень элегантный способ использования шаблона проектирования Builder для исключений. Мне просто нужно посмотреть, насколько легко было бы использовать его с иерархией исключений. - person Claudiu; 06.04.2017
comment
@Claudiu - Как я прочитал этот ответ на другой вопрос , объект исключения инициализируется копированием, что может вызвать конструктор перемещения, если это возможно. Оператору throw также разрешено неявно перемещать временные файлы. Вероятно, стоит провести тест и посмотреть, что произойдет в вашем конкретном случае. - person Karl Nicoll; 06.04.2017
comment
Ты прав. В моем тесте я не реализовал конструктор перемещения, поэтому вместо него был вызван конструктор копирования. Так что это тоже отличное решение. - person Claudiu; 06.04.2017

Ожидается, что классы исключений C++ будут копируемыми или, по крайней мере, перемещаемыми. В вашем примере создание копируемого класса — это вопрос добавления конструктора копирования по умолчанию:

Exception(Exception const&) = default;

Если вам нужно инкапсулировать какое-то некопируемое и неперемещаемое состояние в вашем классе исключений, оберните такое состояние в std::shared_ptr.

person Joseph Artsimovich    schedule 03.04.2017
comment
Хм, а приведенное выше определение конструктора копирования отличается от его неявного определения? - person Claudiu; 03.04.2017
comment
@Claudiu Наличие пользовательского конструктора предотвращает создание неявного конструктора копирования. Вы все еще можете ввести его явно, что я и сделал выше. - person Joseph Artsimovich; 03.04.2017
comment
Разве конструктор копирования не сгенерирован? Если я напишу Exception e("message"); e.set_line(__LINE__); throw e;, он все равно будет работать правильно, но я ожидаю, что объект будет скопирован. - person Claudiu; 03.04.2017
comment
@Claudiu Ты прав, а я ошибался. Пользовательский конструктор не препятствует созданию неявного конструктора копирования. Полный набор правил, касающихся неявных конструкторов копирования и перемещения, можно найти здесь. - person Joseph Artsimovich; 03.04.2017

Вы можете создать класс хранения данных, например ExceptionData. Затем создайте объект ExceptionData и вызовите его методы. Затем создайте объект Exception, используя std::move в ctor, например:

ExceptionData data;
data.method();
throw Exception(std::move(data));

Конечно, ExceptionData должен быть подвижным, и у вас должен быть ctor, который принимает ExceptionData && (ссылка на rvalue).

Это сработает, если вам действительно нужно избегать копий, но мне кажется, что это предварительная оптимизация. Подумайте, как часто в вашем приложении возникают исключения, и действительно ли стоит усложнять ситуацию в этом отношении.

person Aleksei Petrenko    schedule 03.04.2017