254. Exception types in clause 19 are constructed from std::string

Section: 19.2 [std.exceptions], 31.5.2.2.1 [ios.failure] Status: CD1 Submitter: Dave Abrahams Opened: 2000-08-01 Last modified: 2023-02-07

Priority: Not Prioritized

View all other issues in [std.exceptions].

View all issues with CD1 status.

Discussion:

Many of the standard exception types which implementations are required to throw are constructed with a const std::string& parameter. For example:

     19.1.5  Class out_of_range                          [lib.out.of.range]
     namespace std {
       class out_of_range : public logic_error {
       public:
         explicit out_of_range(const string& what_arg);
       };
     }

   1 The class out_of_range defines the type of objects  thrown  as  excep-
     tions to report an argument value not in its expected range.

     out_of_range(const string& what_arg);

     Effects:
       Constructs an object of class out_of_range.
     Postcondition:
       strcmp(what(), what_arg.c_str()) == 0.

There are at least two problems with this:

  1. A program which is low on memory may end up throwing std::bad_alloc instead of out_of_range because memory runs out while constructing the exception object.
  2. An obvious implementation which stores a std::string data member may end up invoking terminate() during exception unwinding because the exception object allocates memory (or rather fails to) as it is being copied.

There may be no cure for (1) other than changing the interface to out_of_range, though one could reasonably argue that (1) is not a defect. Personally I don't care that much if out-of-memory is reported when I only have 20 bytes left, in the case when out_of_range would have been reported. People who use exception-specifications might care a lot, though.

There is a cure for (2), but it isn't completely obvious. I think a note for implementors should be made in the standard. Avoiding possible termination in this case shouldn't be left up to chance. The cure is to use a reference-counted "string" implementation in the exception object. I am not necessarily referring to a std::string here; any simple reference-counting scheme for a NTBS would do.

Further discussion, in email:

...I'm not so concerned about (1). After all, a library implementation can add const char* constructors as an extension, and users don't need to avail themselves of the standard exceptions, though this is a lame position to be forced into. FWIW, std::exception and std::bad_alloc don't require a temporary basic_string.

...I don't think the fixed-size buffer is a solution to the problem, strictly speaking, because you can't satisfy the postcondition
  strcmp(what(), what_arg.c_str()) == 0
For all values of what_arg (i.e. very long values). That means that the only truly conforming solution requires a dynamic allocation.

Further discussion, from Redmond:

The most important progress we made at the Redmond meeting was realizing that there are two separable issues here: the const string& constructor, and the copy constructor. If a user writes something like throw std::out_of_range("foo"), the const string& constructor is invoked before anything gets thrown. The copy constructor is potentially invoked during stack unwinding.

The copy constructor is a more serious problem, becuase failure during stack unwinding invokes terminate. The copy constructor must be nothrow. Curaçao: Howard thinks this requirement may already be present.

The fundamental problem is that it's difficult to get the nothrow requirement to work well with the requirement that the exception objects store a string of unbounded size, particularly if you also try to make the const string& constructor nothrow. Options discussed include:

(Not all of these options are mutually exclusive.)

Proposed resolution:

Change 19.2.3 [logic.error]

namespace std {
  class logic_error : public exception {
  public:
    explicit logic_error(const string& what_arg);
    explicit logic_error(const char* what_arg);
  };
}

...

logic_error(const char* what_arg);

-4- Effects: Constructs an object of class logic_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.4 [domain.error]

namespace std {
  class domain_error : public logic_error {
  public:
    explicit domain_error(const string& what_arg);
    explicit domain_error(const char* what_arg);
  };
}

...

domain_error(const char* what_arg);

-4- Effects: Constructs an object of class domain_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.5 [invalid.argument]

namespace std {
  class invalid_argument : public logic_error {
  public:
    explicit invalid_argument(const string& what_arg);
    explicit invalid_argument(const char* what_arg);
  };
}

...

invalid_argument(const char* what_arg);

-4- Effects: Constructs an object of class invalid_argument.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.6 [length.error]

namespace std {
  class length_error : public logic_error {
  public:
    explicit length_error(const string& what_arg);
    explicit length_error(const char* what_arg);
  };
}

...

length_error(const char* what_arg);

-4- Effects: Constructs an object of class length_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.7 [out.of.range]

namespace std {
  class out_of_range : public logic_error {
  public:
    explicit out_of_range(const string& what_arg);
    explicit out_of_range(const char* what_arg);
  };
}

...

out_of_range(const char* what_arg);

-4- Effects: Constructs an object of class out_of_range.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.8 [runtime.error]

namespace std {
  class runtime_error : public exception {
  public:
    explicit runtime_error(const string& what_arg);
    explicit runtime_error(const char* what_arg);
  };
}

...

runtime_error(const char* what_arg);

-4- Effects: Constructs an object of class runtime_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.9 [range.error]

namespace std {
  class range_error : public runtime_error {
  public:
    explicit range_error(const string& what_arg);
    explicit range_error(const char* what_arg);
  };
}

...

range_error(const char* what_arg);

-4- Effects: Constructs an object of class range_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.10 [overflow.error]

namespace std {
  class overflow_error : public runtime_error {
  public:
    explicit overflow_error(const string& what_arg);
    explicit overflow_error(const char* what_arg);
  };
}

...

overflow_error(const char* what_arg);

-4- Effects: Constructs an object of class overflow_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change 19.2.11 [underflow.error]

namespace std {
  class underflow_error : public runtime_error {
  public:
    explicit underflow_error(const string& what_arg);
    explicit underflow_error(const char* what_arg);
  };
}

...

underflow_error(const char* what_arg);

-4- Effects: Constructs an object of class underflow_error.

-5- Postcondition: strcmp(what(), what_arg) == 0.

Change [ios::failure]

namespace std {
  class ios_base::failure : public exception {
  public:
    explicit failure(const string& msg);
    explicit failure(const char* msg);
    virtual const char* what() const throw();
};
}

...

failure(const char* msg);

-4- Effects: Constructs an object of class failure.

-5- Postcondition: strcmp(what(), msg) == 0.

Rationale:

Throwing a bad_alloc while trying to construct a message for another exception-derived class is not necessarily a bad thing. And the bad_alloc constructor already has a no throw spec on it (18.4.2.1).

Future:

All involved would like to see const char* constructors added, but this should probably be done for C++0X as opposed to a DR.

I believe the no throw specs currently decorating these functions could be improved by some kind of static no throw spec checking mechanism (in a future C++ language). As they stand, the copy constructors might fail via a call to unexpected. I think what is intended here is that the copy constructors can't fail.

[Pre-Sydney: reopened at the request of Howard Hinnant. Post-Redmond: James Kanze noticed that the copy constructors of exception-derived classes do not have nothrow clauses. Those classes have no copy constructors declared, meaning the compiler-generated implicit copy constructors are used, and those compiler-generated constructors might in principle throw anything.]

[ Batavia: Merged copy constructor and assignment operator spec into exception and added ios::failure into the proposed resolution. ]

[ Oxford: The proposed resolution simply addresses the issue of constructing the exception objects with const char* and string literals without the need to explicit include or construct a std::string. ]