22

Consider the following:

std::ostream out(nullptr);

Is this legal and well-defined?


How about if I now do:

out << "hello world\n";

Is this legal and well-defined? If so, presumably it's a no-op of sorts?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • I always add a unit test if I think something like this should work, and appears to be valid. That way if someone builds for another compiler/platform stdlib then they know its broken. – paulm Sep 05 '14 at 16:59
  • 2
    @paulm: I almost agree, except unit tests are not something you should rely on for validating well-definedness. – Lightness Races in Orbit Sep 05 '14 at 17:09
  • Unless it accesses memory out of bounds or something I don't see an issue? But then again unit tests should run with app verifier enabled/valgrind etc – paulm Sep 05 '14 at 17:17
  • 2
    @paulm: It's because you cannot rely on UB to have reliable, reproducible or immediately detectable symptoms. – Lightness Races in Orbit Sep 05 '14 at 18:31
  • 1
    Obscure behavior that you rely on should always be in the test suite even if you find out it is in the standard. Certain things like this could be an unknown bug in some library/compiler's implementation. – Tim Seguine Sep 05 '14 at 18:32
  • @TimSeguine: Again, yes in principle, but in practice you cannot write a unit test to prove that something is well-defined. It is not possible. – Lightness Races in Orbit Sep 05 '14 at 18:34
  • I disagree, UB means anything can happen, the compiler can emit instructions to format your HD etc, but for a given compiler, stdlib and platform it will tend to always do the same thing. Thus if you test for what you expect on that platform/compiler/config then you can "get away" with it. For things like reading/writing memory out of bounds not so much, but again app verifer or valgrind would catch this to remove the "randomness". – paulm Sep 05 '14 at 19:20
  • @paulm: Nonsense, as your final sentence itself explains. UB on the whole is _not_ dependable, even within the same system. Yes, you need something like Valgrind to catch that stuff, and even then you won't always get it. Just reaffirming that unit tests are not always appropriate or useful, and can't magically "detect" UB in the general case. – Lightness Races in Orbit Sep 05 '14 at 19:29
  • So you're saying if I use valgrind and read/write memory out of bounds it won't be detected? Now that sounds like nonsense to me. The issue there would be that for some reason the code only sometimes reads or writes out of bounds. – paulm Sep 05 '14 at 20:12
  • @paulm: No, I'm saying that running valgrind is not a unit test. And, yes, it's not always going to detect your bugs — you just gave a really good example of when that can fail. – Lightness Races in Orbit Sep 05 '14 at 21:27
  • Just for the record I wasn't disagreeing with your point. – Tim Seguine Sep 05 '14 at 21:37
  • I still don't get your point, so valgrind will find the problem, unless there isn't a problem. In my example it would be if the out of bounds read/write was "random", but that's never the case in my experience. Usually this will be because of some other issue such as un-initialized variable which it will also flag before the out of bound r/w. – paulm Sep 05 '14 at 21:42
  • @paulm: In `std::vector v(5); std::cout << v[rand() % 10];` there is most definitely a problem, but Valgrind will only detect it 50% of the time _at best_ (that percentage is actually lower, because Valgrind will also fail here if the memory immediately following the vector's data also happens to have been allocated beforehand). If you've not put proper assertions in place then it's quite easy to end up with this sort of bug thanks to user-input. It's not ill-formed, it's UB, and you can't automatically, _reliably_ detect it at runtime in the general case. That's what I'm saying! – Lightness Races in Orbit Sep 05 '14 at 21:43

1 Answers1

26

Yes, it is legal and well-defined to instantiate that stream. You can safely swap it with another stream, or give it a new pointer (this time to an extant buffer) at a later time. The output operation itself is indeed a no-op.

Here's why:

  1. The construction has no non-null precondition, and has only this postcondition:

    [C++11: 27.7.3.2/2]: Postcondition: rdbuf() == sb.

  2. Interestingly, it makes an explicit point that no operation shall be performed on sb within the constructor:

    [C++11: 27.7.3.2/4]: Remarks: Does not perform any operations on rdbuf().

  3. But note also:

    [C++11: 27.7.3.2/1]: Effects: Constructs an object of class basic_ostream, assigning initial values to the base class by calling basic_ios<charT,traits>::init(sb) (27.5.5.2).

  4. That init(sb) call has the effect of setting badbit on the stream when sb is NULL:

    [C++11: 27.5.5.2/3]: Postconditions: The postconditions of this function are indicated in Table 128.

    [C++11: Table 128]: [..] rdstate(): goodbit if sb is not a null pointer, otherwise badbit. [..]

  5. The output operation would result in actions equivalent to dereferencing a null pointer:

    [C++11: 27.7.3.1/2]: Two groups of member function signatures share common properties: the formatted output functions (or inserters) and the unformatted output functions. Both groups of output functions generate (or insert) output characters by actions equivalent to calling rdbuf()->sputc(int_type). They may use other public members of basic_ostream except that they shall not invoke any virtual members of rdbuf() except overflow(), xsputn(), and sync().

    except it never gets this far, because for basic_ostream::sentry construction:

    [C++11: 27.7.3.4/3]: If, after any preparation is completed, os.good() is true, ok_ == true otherwise, ok_ == false.

    and, for explicit operator basic_ostream::sentry::bool() const;:

    [C++11: 27.7.3.4/5]: Effects: Returns ok_.

    and:

    [C++11: 27.7.3.7/1]: Each unformatted output function begins execution by constructing an object of class sentry. If this object returns true, while converting to a value of type bool, the function endeavors to generate the requested output. [..]

    …the implication being that no output operation takes place at all when badbit is already set.

This was also the case in C++03.

Community
  • 1
  • 1
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055