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

У меня есть класс 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.

Ответ 1

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

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

Кроме того, вы можете создать построитель Java-esque, например:

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();

Ответ 2

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

Exception(Exception const&) = default;

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

Ответ 3

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

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

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

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