10

I have an Exception class on which I want to set more information before I throw it. Can I create the Exception object, call some of its functions and then throw it without any copies of it being made?

The only method I've found is throwing a pointer to the object:

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) {...}

But throwing exceptions by pointer is generally avoided, so is there any other way?

There is also the option of setting all the options through the constructor, but this can quickly become unscalable if more fields are added to the class and you want to have fine-grained control over what fields to set:

throw Exception("message"); // or:
throw Exception("message", __LINE__); // or:
throw Exception("message", __FILE__); // or:
throw Exception("message", __LINE__, __FILE__); // etc.
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Claudiu
  • 2,124
  • 4
  • 26
  • 36
  • 2
    Adding fields through function calls "can quickly become unscalable". So, IMHO adding them through a constructor is the best practice. – Jonas Apr 03 '17 at 12:08
  • 1
    @Jonas What I meant is that it can become unscalable if you want to have fine-grained control over what options you want to set. I will update the question. – Claudiu Apr 03 '17 at 12:11
  • 1
    Yes, that may be problematic. This is one of the reasons that "named-parameters" would be a nice feature. – Jonas Apr 03 '17 at 12:13
  • 4
    No need to create `e` as a pointer. Re your 4 throw examples use default parameters, so you only need to define 1 constructor. – Richard Critten Apr 03 '17 at 12:13
  • 1
    @RichardCritten Good observation, indeed default parameters would help. I see two possible drawbacks though: 1. if the value that interests you is the last parameter you would need to manually fill all the other parameters with default values; 2. if there are many parameters you will end up with constructors looking like the CreateWindow function [bleh] – Claudiu Apr 03 '17 at 13:14
  • 1
    Just to mention the idea: a class hierarchy (like std::exception) might be appropriate instead of a generic Exception class that tries to encapsulate everything. – Andre Kostur Apr 03 '17 at 13:44
  • 1
    Does Boost.Exception address your actual problem (putting lots of different things in an exception)? – Jeffrey Bosboom Apr 03 '17 at 19:46
  • @JeffreyBosboom Unfortunately I can't use boost in my project, but this may be a good resource for those who can. Using multiple inheritance to create new exception types is definitely an interesting idea. – Claudiu Apr 04 '17 at 08:01

3 Answers3

8

C++ exception classes are expected to be copyable or at least movable. In your example, making your class copyable is a matter of adding a default copy constructor:

Exception(Exception const&) = default;

If you need to encapsulate some non-copyable and non-movable state in your exception class, wrap such state into std::shared_ptr.

Joseph Artsimovich
  • 1,499
  • 10
  • 13
  • Hm, is the above definition of the copy constructor different than its implicit definition? – Claudiu Apr 03 '17 at 15:42
  • 1
    @Claudiu The presence of a custom constructor prevents the implicit copy constructor from being generated. You can still introduce it explicitly, which is what I did above. – Joseph Artsimovich Apr 03 '17 at 15:51
  • 1
    Isn't the copy constructor generated anyway? If I write `Exception e("message"); e.set_line(__LINE__); throw e;` it will still work correctly, but I expect the object to be copied. – Claudiu Apr 03 '17 at 16:19
  • @Claudiu You are right and I was wrong. A custom constructor doesn't prevent an implicit copy constructor from being generated. The full set of rules covering implicit copy and move constructors can be found [here](http://en.cppreference.com/w/cpp/language/copy_constructor). – Joseph Artsimovich Apr 03 '17 at 16:47
5

You can create a data-holding class, like ExceptionData. Then create ExceptionData object and call it's methods. Then create Exception object using std::move in ctor like:

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

Of course, ExceptionData needs to be movable and you have to have ctor that accepts ExceptionData && (rvalue reference).

It'll work if you really need to avoid copies, but to me it feels like preliminary optimization. Think how often exceptions are being thrown in your app and is it really worth it to complicate things for that matter.

Aleksei Petrenko
  • 6,698
  • 10
  • 53
  • 87
2

What about using std::move?

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

Alternatively, you can create a Java-esque builder like this:

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

Used like this:

throw Exception::create("TEST")
    .with_line(__LINE__)
    .with_file(__FILE__)
    .finalize();
Karl Nicoll
  • 16,090
  • 3
  • 51
  • 65
  • Regarding the std::move approach - still two copies of the exception are being created. In Meyers' More Effective C++, he says: "C++ specifies that an object thrown as an exception is always copied", so I think this might also be the case here. – Claudiu Apr 06 '17 at 10:39
  • I like the second approach - it's a very elegant way of using the Builder design pattern for exceptions. I just need to see how easy it would be to use it with a hierarchy of exceptions. – Claudiu Apr 06 '17 at 10:41
  • @Claudiu - The way I read [this answer to another question](https://stackoverflow.com/questions/25534628/using-the-move-constructor-to-throw-exceptions-c), the exception object is copy initialized, which may invoke the move constructor if it can. The throw statement is also permitted to implicitly move temporaries. It's probably worth doing a test and seeing what happens in your specific case though. – Karl Nicoll Apr 06 '17 at 10:58
  • You are correct. In my test I didn't implement the move constructor so the copy constructor was called instead. So this is a great solution too. – Claudiu Apr 06 '17 at 12:07