c++language-lawyerundefined-behavioriostream

Is it undefined behavior to pass a pointer to an unconstructed streambuf object to the ostream constructor?


Question

Does the following program have undefined behavior?

#include <iostream>          // std::{ostream, streambuf}

// The streambuf ctor is protected so we need a wrapper to create one.
struct mystreambuf : public std::streambuf {};

extern mystreambuf sb;       // Not yet constructed.
std::ostream os(&sb);        // Passing "invalid" pointer here?  UB?
mystreambuf sb;              // Now it is constructed.

int main() { return 0; }

It invokes the ostream constructor, passing a pointer to a streambuf object whose lifetime has not yet begun (basic.life p1). Does this constitute undefined behavior?

Attempted answer

If streambuf were a user-written class, then class.cdtor p1 would govern, which says:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior. [...]

This language, and its accompanying example, make it clear that merely taking the address of an unconstructed object is not undefined. As far I can tell, passing that address as a pointer to a user-written function that only stores its value and tests it against nullptr is also not undefined.

But streambuf is a library class, so instead res.on.arguments p1 applies, which says, in part:

If an argument to a function has an invalid value (such as a value outside the domain of the function or a pointer invalid for its intended use), the behavior is undefined.

But what constitutes an "invalid value"? Presumably we have to determine the "intended use" by reading the specification of the called function. The constructor spec ostream.cons p1 says in part:

Effects: Initializes the base class subobject with basic_ios<charT, traits>::init(sb) ([basic.ios.cons]).

The spec for init basic.ios.cons p4 says:

Postconditions: The postconditions of this function are indicated in Table 127.

where Table 127 has two rows that mention sb:

Element    Value
-------    -----
rdbuf()    sb
rdstate()  goodbit if sb is not a null pointer, otherwise badbit.

So, at first glance, this would seem to suggest that sb is only stored (so that rdbuf() can return it) and tested for being nullptr; and that these together comprise its "intended use". Since both of these would be legal for user-written code to do, it is legal to pass the pointer in question, so the program has defined behavior.

But Table 127 is merely a list of postconditions. It does not definitively assert that nothing else is in the scope of "intended use". For that, it would seem necessary to exhaustively review everything that basic_ostream and its subclasses potentially do with sb.

While attempting to do so, I find imbue at basic.ios.members p9:

Effects: Calls ios_base::imbue(loc) and if rdbuf() != 0 then rdbuf()->pubimbue(loc).

Clearly, calling rdbuf()->pubimbue(loc) before the object pointed to by rdbuf() is constructed is undefined. Do we call imbue? Not explicitly of course, and there's no particular reason to suspect an indirect call either, but the existence of this behavior arguably puts it in scope of the "intended use" of the pointer passed to the constructor, since eventually it could be used this way. Furthermore, would it necessarily be non-conforming for an implementation to call imbue on its own during the ostream constructor? I don't see why it would be, and if an implementation is free to call imbue in the constructor, then clearly we have undefined behavior. And there could be other methods that suggest other usages, as my survey was by no means complete.

Now, in a comment on an answer to a related question, indi observes that the Clang implementation of std::basic_fstream does pass a pointer to an unconstructed member object to the iostream constructor at fstream:1419:

  basic_filebuf<char_type, traits_type> __sb_;
};

template <class _CharT, class _Traits>
inline basic_fstream<_CharT, _Traits>::basic_fstream() : basic_iostream<char_type, traits_type>(&__sb_) {}

But this example is not definitive because (1) it could be a mistake, and (2) the library implementation is generally allowed to do things that would be undefined in user code. Nevertheless, it is at least weak evidence that the Clang developers think the practice does not have undefined behavior, as they have no reason in this case to write code that relies on the library's license to bend the rules, since it would be a trivial change to instead pass nullptr to the constructor and then in the body call init with the address of the (now fully constructed) member object.

Ultimately, it seems to me that the language specification is ambiguous, as it relies on the terms "invalid value" and "intended use" which are not clearly specified. But perhaps someone can identify a provision I have missed or an error in my interpretations.

Related questions

While researching this, I came across some existing questions that seemed related. The question How to inherit from std::ostream? has three relevant answers:

From these answers and comments, I infer that quite a few knowledgeable people believe that the example at the top of this question has undefined behavior.

Meanwhile, the question Is it dangerous to pass a pointer to a subobject that is not constructed yet to a constructor of another subobject during the object construction? is very nearly the same as mine, but is marred by having some important parts of the example code missing, and involves an extraneous AnotherClass that further muddies the question. The answer by aschepler seems to say that the practice is ok in general, but not in the OP's case because of AnotherClass, but it only reasons as if all of the code were written by the user, ignoring the library aspect.

Finally, the question Is it safe to pass an unconstructed buffer to the constructor of std::ostream? is essentially the same as mine--I'm asking a duplicate! Why? In short, that question has no answers, and I think the additional research in my question makes it more likely mine can be answered, so I'm effectively submitting this with the intention of replacing that one. I asked a meta question about whether asking this duplicate is acceptable, and the consensus seems to be that is.


I've accepted Chris Dodd's answer, but I want to elaborate a little on it, so this is a restatement of that answer in my own words.

The original example has undefined behavior because, in this line:

std::ostream os(&sb);        // Passing "invalid" pointer here?  UB?

the expression &sb has type mystreambuf*, but is being passed to a constructor that accepts std::streambuf*, and therefore must undergo derived-to-base conversion. That conversion, applied to a pointer to an unconstructed object with non-trivial constructor, has undefined behavior since it is a "[reference] to any [...] base class of the object", which is prohibited by class.cdtor p1.

The example in that section further clarifies. Quoting the key lines from it:

struct X { int i; };
struct Y : X { Y(); };                  // non-trivial
struct A { int a; };
struct B : public A { int j; Y y; };    // non-trivial

extern B bobj;
A* pa = &bobj;                          // undefined behavior: upcast to a base class type
B bobj;                                 // definition of bobj

Moreover, this means that not only is the specific example in the question undefined, but it is in general undefined to do what the question title says, namely to "pass a pointer to an unconstructed streambuf object to the ostream constructor". That is because the std::streambuf constructor is protected, so an instance must always be a proper base class subobject, and therefore the only way to obtain a std::streambuf* is with a derived-to-base conversion.

That implies that the code quoted from the Clang libc++ would have undefined behavior if it were user code, and I have filed Issue #93307 against Clang about that.


Solution

  • The language you quoted

    For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior. [...]

    would seem to indicate this is undefined behavior -- you're referring to the base class (std::streambuf) of an object before the constructor has run. What happens in the ostream constructor is irrelevant.