First situation is the std::istream interface for reading the data;
that is, the operator >>. If you want to read a piece of data from the
stream interface, you first have to create an object with a useless
value and then override that value with operator >>, like in the below
example:
T val;
std::cin >> val;
If T was a typedef for int, the problem is not that big, but still
until we do the stream extraction, the value is indeterminate. For a
user defined type, the problem may be bigger, because I might not like
the default constructor at all, but I was forced to create it to be
able to use operator >>, or otherwise I would have been forced to
abandon operator >> for my type if I wanted a one-stage construction.
Reading from the stream may not be the best way of initializing an
object (I don't really know), but I have seen it used in
boost::lexical_cast and in currently developed boost::convert. In
order for boost::convert to work with user types that do not support
default construction, the user is required to provide a value of that
type only to replace default construction requirement with copy
construction.
Second situation is when two objects need to be constructed and they
need to refer to one another. For illustration, the below is an
example of symmetric coroutines from boost::coroutine:
producer_type producer;
consumer_type consumer;
producer = producer_type( bind(
producer_body,
_1,
"hello",
ref(consumer)
));
consumer = consumer_type( bind(
consumer_body,
_1,
_2,
ref(producer)
));
Here because producer coroutine needs a reference to consumer
coroutine and vice versa they have to be initialized in two stages.
Now, as to the purpose of this post, I was trying to identify classes
of problems where the pattern "constructor establishes the class
invariant" cannot be applied. I would appreciate any comment on if the
problems above could have been solved with one-stage initialization,
but I failed to see how, or if there are any other classes of problems
that require a two-stage initialization.
Regards,
&rzej
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
Say what you mean please.
That's not what RAII means as commonly defined and understood. RAII is
the idiom / pattern that for every dynamic allocation of some
resource, you should immediately give that resource an owner, an
object which will free the resource in its destructor. (Preferably
done so that the ownership graph is acyclic). (In particular, all
objects are resources, memory.) In this idiom, ownership is not
unchangeable, nor must there be a single owner, but all resources at
all times should have an owner; that is RAII.
As stack objects are guaranteed to be destroyed when the current stack
frame is exited, either by caught exceptions or return, we are
guaranteed those objects will be destroyed, and all resources those
objects own, directly or indirectly, will be freed. Following this
idiom makes leaks rare. Also, following this idiom makes leaks much
easier to fix. It allows you easily reason about leaks: "How is this
object getting leaked? It must not have an owner at some point. Let's
find out where ownership was lost and give it an owner."
RAII is an idiom of resource ownership and not an idiom of class
invariants, though the two concepts are related.
> Now, as to the purpose of this post, I was trying to identify classes
> of problems where the pattern "constructor establishes the class
> invariant" cannot be applied. I would appreciate any comment on if the
> problems above could have been solved with one-stage initialization,
> but I failed to see how, or if there are any other classes of problems
> that require a two-stage initialization.
As to actually answer your question:
//As commonly done for streams
int x;
stream >> x;
//As commonly done in most other places
int x = stream.getInt();
Why are iostreams done the first way? I'm not quite sure, though I'd
guess history, to look similar to scanf printf et al., and ease of
use. Chaining << and >> might be easier to use and look better than
many calls to getInt(), especially when you can test the result of
(stream >> x) in an if statement, and you can't check the result of
int x = stream.getInt() in the same line.
Also, barring some convoluted definitions of classes, I'd expect that
you could have single initialization in most places, though maybe it's
more intuitive or easier to read as two stage initialization, or maybe
efficiency concerns. Frankly, this is a very large topic on which I'm
ill prepared to speak.
IMHO, std::logic_error.
A constructor _always_ establishes the class invariants. It only
depends how well those invariants were defined (and implemented).
On the other hand, not all classes are designed (meant) to be
used for RAII.
> First situation is the std::istream interface for reading the data;
> that is, the operator >>. If you want to read a piece of data from the
> stream interface, you first have to create an object with a useless
> value and then override that value with operator >>, like in the below
> example:
>
> T val;
> std::cin >> val;
>
> If T was a typedef for int, the problem is not that big, but still
> until we do the stream extraction, the value is indeterminate. For a
The value created by the default constructor is well defined and
determinate. You did not make a successful attack on RAII,
but only on the serialization of objects.
From the top of my mind (someone might provide a better design),
you could use a smart pointer, or a custom wrapper:
std::shared_ptr<T> val;
std::cin >> val;
And there you have RAII handled by '*val (T)' instead of 'val'.
Your version of T is clearly not meant to be a RAII class. So basically,
you've accused a child of acting like a child... :-D
On the personal note, cease and desist all attacks on RAII!
It's the number one reason I (and many of us) like C++. :-)
--
Dragan
That's a flaw in the standard streams design.
Two alternative designs that don't have that problem:
- Return the values.
- Invoke functions when extracting the values, similarly to
Boost.Spirit semantics actions.
As for your recursive definition situation, you could use dirty tricks
along these lines:
consumer_pod_type consumer;
producer_type producer = producer_type( bind(
producer_body,
_1,
"hello",
reinterpret_cast<consumer_type>(&consumer)
));
new(&consumer) consumer_type = consumer_type( bind(
consumer_body,
_1,
_2,
ref(producer)
));
I suppose you could achieve that kind of thing cleany with a DSEL.
Actually, a constructor that can not establish a class invariant must not
return, it must either throw (which is the default error signalling
mechanism) or it could in very rare cases invoke exit(). However,
establishing class invariants is the goal of the constructor, so it _must_
do that.
I guess you mean something else. Two examples are smart pointers or file
streams. Both can finish the constructor but be NULL or have their failbit
set. However, this is not a class invariant!
> Now, as to the purpose of this post, I was trying to identify classes
> of problems where the pattern "constructor establishes the class
> invariant" cannot be applied.
Sorry, but this pattern by definition applies to all correct code. What you
have is a two-stage construction, where parts of an object are not usable
until a second initialisation took place. This however is not an invariant,
this is something that can and does change!
Uli
--
Sator Laser GmbH
Geschäftsführer: Thorsten Föcking, Amtsgericht Hamburg HR B62 932
I didn't really mean RAII. I meant the "pattern" where the constructor
establishes an invariant. The invariant that is not only well defined,
but also requires no runtime checks of the object state/invariant in
calls to subsequent member functions. I just didn't know how to put
this idea in the post's subject and keep the subject short at the same
time. Perhaps "Single-phase initialization not always possible" would
be more suitable.
My intention was not to prove that single-phase init was useless, by
showing examples. I find it super-useful in terms of code brevity,
safety and elegance. No other programming language offers more elegant
a solution (at least as far as I know). I was rather trying to find
out if this pattern is applicable to 90% or 100% of initialization
cases. I cannot help noticing "suspect" (IMO) situations, so I shared
them in order for someone more skilled than me to say that either
there is a better way to do it, or there are some cases that really
require two-phase init.
In case of "recursive definition example" I don't think that I
understood the solution:
>> I suppose you could achieve that kind of thing cleany with a DSEL.
If I reduce the example to:
producer_type producer;
consumer_type consumer;
producer = producer_type( consumer );
consumer = consumer_type( producer );
How would the DSEL for making that a single-phase init look like?
Regards,
&rzej
Streams are done the way that they are because it is extendable to new
user defined types:
istream::getInt()
would be a method and new methods cannot be added to classes without
editing the source -
When you create a class X - how are you going to add istream::getX() ?
istream::operator>>(istream&, X&)
is a free function that you can write anytime.
I think that what you want is a new constructor:
X::X(istream&)
The trouble with this is that you have to do a lot of exception
handling rather than just checking stream state.
> If I reduce the example to:
>
> producer_type producer;
> consumer_type consumer;
>
> producer = producer_type( consumer );
> consumer = consumer_type( producer );
>
> How would the DSEL for making that a single-phase init look like?
Maybe something like this:
tuple<producer_type, consumer_type> coroutines = recursive_def(
construct_<producer_type>(_2),
construct_<consumer_type>(_1)
);
I don't buy that. It could have been done analogously with
freestanding functions. ex:
int std::extractInt(std::istream& );
user_defined_t user_namespace::extractUserDefinedT(std::istream& );
It still may be because of being able to do the extract and test the
stream in the same expression ala if (istream >> x)
"Class invariant" is not such a bad catchphrase, I think.
>> Much as I find the concept useful, I found at
>> least two situations where it cannot be applied.
>
> IMHO, std::logic_error.
>
> A constructor _always_ establishes the class invariants. It only
> depends how well those invariants were defined (and implemented).
It is obvious that "restor" realizes that, and that he really wants
more *useful*, stronger invariants. Not having a default constructor,
and so on.
I share his experience, and it annoys me daily. I try to just accept
it as a fact of life: that some of my classes will not have a useful
invariant, and that I have to be careful when handling them.
...
> determinate. You did not make a successful attack on RAII,
...
> Your version of T is clearly not meant to be a RAII class. So basically,
> you've accused a child of acting like a child... :-D
...
> On the personal note, cease and desist all attacks on RAII!
> It's the number one reason I (and many of us) like C++. :-)
As I read the posting, he's not attacking or accusing -- he just wants
more of his objects to have a useful invariant. I'm sure many of us
are interested in hearing about techniques which can help. (Although
I'm personally not interested in stream input -- I much prefer reading
lines of text and doing my own parsing.)
/Jorgen
--
// Jorgen Grahn <grahn@ Ph'nglui mglw'nafh Cthulhu
\X/ snipabacken.se> R'lyeh wgah'nagl fhtagn!
I read between the lines and I think I know what you mean. You want to
initialize objects directly to reduce state within the object. That is
you don't have to check for initialization conditions when using the
object. A small silly example:
// invariant: name.size() > 5
struct InitMe {
InitMe() : initialized(false) {} //does not fully init
void init(std::string const& newName)
{
if(initialized)
throw std::runtime_error("...");
if(newName.size()<=5)
throw std::runtime_error("...");
name = newName;
initialized = true;
}
void printName() //needs initialized==true
{
if( ! initialized) //I don't want to check this
throw std::runtime_error("...");
std::cout.write(name.c_str(), 5);
}
private:
std::string name;
};
InitMe me;
std::string newName;
stream >> newName;
me.init(newName);
me.printName();
restor schrieb:
> First situation is the std::istream interface for reading the data;
> that is, the operator >>. If you want to read a piece of data from the
> stream interface, you first have to create an object with a useless
> value and then override that value with operator >>, like in the below
> example:
>
> T val;
> std::cin >> val;
To me the obvious way to do this is (or "work around"):
// invariant: name.size() > 5
struct InitMe2 {
InitMe(std::string const& newName)
: name(newName)
{
if(newName.size()<=5)
throw std::runtime_error("...");
}
void printName()
{
std::cout.write(name.c_str(), 5);
}
private:
const std::string name; //can make const
};
std::string newName;
stream >> newName;
InitMe2 me(newName);
This approach also gives opportunity to make the class members const
(like InitMe2::name) or even to make the class instance "me" const.
The general approach would either pass all needed variables to the ctor
or use a dedicated struct with all necessary data. This struct could
also define an operator >> to read itself from a stream. The struct
could also be placed inside the class:
struct InitMe3 {
struct Data {
std::string name;
};
InitMe3(Data const& newData)
: data(newData)
{
if(data.name.size()<=5)
throw std::runtime_error("...");
}
void printName()
{
std::cout.write(data.name.c_str(), 5);
}
private:
const Data data;
};
> Second situation is when two objects need to be constructed and they
> need to refer to one another. For illustration, the below is an
> example of symmetric coroutines from boost::coroutine:
[snip]
> Here because producer coroutine needs a reference to consumer
> coroutine and vice versa they have to be initialized in two stages.
I cannot suggest anything much useful. Setting up references can be done
via pointers to raw storage.
//ignore alignment for a while:
char buf[sizeof(Producer)];
Consumer consumer(buf); //only receives raw buffer
new(buf) Producer(consumer);
The problem here is not to allow the consumer access to the producer
until the producer is constructed. This will require some call to the
consumer to notify it of this:
consumer.notifyProducerIsConstructed();
This is again a two stage init. I think it cannot be avoided.
Frank
You're right. Terminology aside, the problem is valid.
I still think that you can refactor any class that has "construct dummy,
initialize later" behavior into two classes: a shared_ptr-like handler
and a _real_ class with stronger encapsulation and invariants.
Maybe the problem is sometimes developers rush to get a (relatively
less important) functionality done, and end up with one bulk class
instead of a cleaner design (I know I have :-) I would like to hear
comments on this.
About OP's second example... A real-world example would help in finding
a better design.
--
Dragan
> I actually didn't mean RAII, but a more general concept that could be
> called "constructor establishes the class invariant".
The (nontrivial) constructor _always_ establishes the class invariant.
If you think it doesn't in some case, someone has mis-documented the
invariant. Believe me, I know what you mean, but I've been down that
road and have personally found that the only way to keep sane is to be
honest about what "invariant" means.
> I wish it had a name as popular as RAII. Much as I find the concept
> useful, I found at least two situations where it cannot be applied.
Then you're wishing for a stronger invariant than the one you actually
have.
> First situation is the std::istream interface for reading the data;
> that is, the operator >>. If you want to read a piece of data from the
> stream interface, you first have to create an object with a useless
> value and then override that value with operator >>, like in the below
> example:
>
> T val;
> std::cin >> val;
Yeah, that's really annoying. That's a trivial constructor, though. If
it were a type with a nontrivial constructor, the invariant would be
established.
> If T was a typedef for int, the problem is not that big, but still
> until we do the stream extraction, the value is indeterminate.
>
> For a
> user defined type, the problem may be bigger, because I might not like
> the default constructor at all, but I was forced to create it to be
> able to use operator >>, or otherwise I would have been forced to
> abandon operator >> for my type if I wanted a one-stage construction.
operator>> is not a good general choice for deserialization because
of this problem. That's why you'll see another option in the
Boost.Serialization library.
It can always be applied: you just need to weaken the ideal invariant to
correspond to reality.
> I would appreciate any comment on if the problems above could have
> been solved with one-stage initialization, but I failed to see how, or
> if there are any other classes of problems that require a two-stage
> initialization.
I think that technically speaking, your desired producer/consumer
invariant can be established in one stage, but you'd need to have the
producer construct the consumer or vice-versa.
I guess the question you're really asking is: “what sorts of invariants
can't be established in one stage?” That's an interesting one, and I
don't have the answer :-)
--
Dave Abrahams
BoostPro Computing
http://www.boostpro.com
If one doesn't need exceptions when using the
X x;
in >> x;
approach, then why would one need them when using a constructor?
X x( in );
Either way, if one wants to check for a read error, one must either check
the stream state or ask x whether the read was successful (or have x throw
an exception).
> I don't buy that. It could have been done analogously with
> freestanding functions. ex:
>
> int std::extractInt(std::istream& );
> user_defined_t user_namespace::extractUserDefinedT(std::istream& );
Then you have need to name the type in the extractor and place you're storing,
Foo foo = extractFoo( in );
which fails for templates, unless you make the extract function a template.
> It still may be because of being able to do the extract and test the
> stream in the same expression ala if (istream >> x)
I thought it was the ability to read multiple items in the order they
appear, that an overloaded operator is used:
in >> x >> y >> z;
That seems like a disaster for generic programming.
template <typename T>
void extract(std::istream& is, T& t)
{
// ??? instead of 'is >> t'
}
--
Seungbeom Kim
> > A constructor _always_ establishes the class invariants. It only
> > depends how well those invariants were defined (and implemented).
>
> It is obvious that "restor" realizes that, and that he really wants
> more *useful*, stronger invariants. Not having a default constructor,
> and so on.
>
> I share his experience, and it annoys me daily. I try to just accept
> it as a fact of life: that some of my classes will not have a useful
> invariant, and that I have to be careful when handling them.
I've found that, no matter how I tried, for classes representing
resources some kind of "NULL" state is always valid state. My
experience is mainly about I/O devices, which become "NULL" after some
kind of close().
Trying to keep the default constructor out of these classes would have
the benefit of making a whole category of runtime errors impossible,
mainly operating on an invalid device handle.
The presence of some kind of close() operation, or C++0x move
constructors, or even the possibility of "volatile invalidation",
imply that someone, somehow, might call one of these operations on an
invalid object; therefore, error checks, throws and catches become
unavoidable.
To me, RAII is really all about destruction and resource release. I've
come to expect little of constructors on such classes.
--
P.
--
I am trying to imagine an example of the problem you described. My
thirst thought is that there must be a way to implement this case with
single-phase-init. Of course I might be wrong from the start, because
my experience with I/O devices is a NULL-experience, but let me just
try. Suppose I have a device class:
class Device
{
public:
Device(); // dummy init
open(); // real init
close();
Data read();
};
The usage in the code is:
Device dev;
// do something here
// calling dev.read() here is an error
dev.open();
Data data1 = dev.read();
use( data1 );
dev.close();
// do something here
// calling dev.read() here is an error
dev.open(); // re-open later
Data data2 = dev.read();
use( data2 );
dev.close();
// do something here
// calling dev.read() here is an error
If I split Device into two classes:
class Device
{
public:
Device(); // dummy init
private:
friend class DeviceSession;
open(); // real init
close();
Data read();
};
class DeviceSession
{
Device & dev;
public:
explicit DeviceSession( Device & d ) : dev(d) { dev.open(); }
~DeviceSession() { dev.close(); }
Data read() { return dev.read(); }
};
The usage could be:
Device dev;
// do something here
// calling dev.read() here is impossible
{
DeviceSession session(dev);
Data data1 = session.read();
use( data1 );
}
// do something here
// calling dev.read() here is impossible
{
DeviceSession session(dev);
Data data2 = session.read();
use( data2 );
}
// do something here
// calling dev.read() here is impossible
Calling read in an improper place is impossible, because Device simply
does not expose "read" interface. Now, this is done at the expense of
introducing a new and possibly counterintuitive class. It assumes that
those sessions do not overlap (otherwise it would not be possible to
insert overlapping nested scopes). And, this is only a scenario I
thought of. Perhaps the case you are thinking of is more compilcated
than this.
> Trying to keep the default constructor out of these classes would have
> the benefit of making a whole category of runtime errors impossible,
> mainly operating on an invalid device handle.
Exactly.
> The presence of some kind of close() operation, or C++0x move
> constructors, or even the possibility of "volatile invalidation",
> imply that someone, somehow, might call one of these operations on an
> invalid object; therefore, error checks, throws and catches become
> unavoidable.
You list three sources of problems that I find very different:
1. The close operation.
I would thinkk that additional "state" classes as in the above example
could out-rule this problem.
2. Move constructors.
In C++0x (as far as I understand it) classes are moveable only if you
make them moveable. If for your class move does not make sense or is
dangerous, you just don't implement move constructors/assignments.
3. Volatile invalidation.
You are right. runtime checks cannot be avoided here.
In the end, your point on volatile invalidation does prove that you
cannot avoid the "closed" state. This is the kind of an example I was
looking for.
Regards,
&rzej
If we take a plain (and perfect) example of RAII usage. Il looks like
this:
{
RAIIResource resource;
resource.use(); /// very simple usage
} // auto-release
Now, it may be insufficient if we need to know the address of the
object before it is initialized (e.g. to pass it by reference), as in
your example:
> you could use a smart pointer, or a custom wrapper:
> std::shared_ptr<T> val;
> std::cin >> val;
First, I would use boost::optional rather than std::shared_ptr, unless
shared ownership was really required. The former doesn't allocate
anything on the heap and is therefore faster.
But the problem that still remains is that whereas T can now be a full
RAII objct, the usage is still a two-phase init with all its burden:
std::shared_ptr<T> val; // dummy init
std::cin >> val; // perhaps full init
if( val ) { // defensive if required :(
val->use();
}
You could call this pattern "Making two-phase init out of single-phase
init class". It is very useful in some situations. I found it useful
once when implementing a problem solver. You would request searching
for the best solution (but usually it means infinately long) and
request a stop and return of the best solution found so far:
void OnButtonSolve() {
solver.start();
}
void OnButtonStop() {
Solution sol = solver.stop();
store( sol );
}
Functions 'start' and 'stop' were respectively creating and joining
+destroying a boost::thread, which is somewhat RAII, in-place with the
help of boost::optional.
> About OP's second example... A real-world example would help in finding
> a better design.
The example I provided is from the being-developed Boost library:
Boost.Coroutine. You can obtain it from Boost Vault:
I didn't want to quote too much in the post. It has a good
documentation, and the example is taken literarly from there.
Regards,
&rzej
--
See below...
>> you could use a smart pointer, or a custom wrapper:
>> std::shared_ptr<T> val;
>> std::cin >> val;
>
> First, I would use boost::optional rather than std::shared_ptr, unless
> shared ownership was really required. The former doesn't allocate
> anything on the heap and is therefore faster.
> But the problem that still remains is that whereas T can now be a full
> RAII objct, the usage is still a two-phase init with all its burden:
My proposal fixed the problem of having a non-RAII class to maintain.
It helps keep things simple and clean.
You are right, nothing has changed outside. But this was intentional.
The initial problem was given in the terms of the usage. I proposed
a solution that will give a better design, yet provide the same
use case.
Now, the way RAII class can be used may be altered by providing
a wrapper instead of 'shared_ptr' or 'optional'. You can provide
_any_ interface you would like. It may throw on "full init",
you may have a function "is_initialized()", anything goes...
About having a reference before initialization, you might be
able to use a reference to the wrapper. Something like this:
SomeResource abstract class with virtual function use().
RAII_SomeResource - implements SomeResource, a clean RAII class.
SomeResource_wrapper - implements SomeResource, handles
two-phase init, delegates to RAII_SomeResource,
can use placement-new for allocation.
I know, now it's three classes instead of one. Maybe this is why
people usually write one complex class. I think, at least concerning
the principle of separation of responsibilities and encapsulation,
it _is_ a good solution. However, I'm not sure it's usable as
a generic solution. If someone finds it usable, please let me know.
> The example I provided is from the being-developed Boost library:
> Boost.Coroutine. You can obtain it from Boost Vault:
>
>
http://www.boostpro.com/vault/index.php?PHPSESSID=ebfe7ae73e7578929b21aaa732
182fe1&direction=0&order=&directory=Concurrent%20Programming
I'll take a look at it...
--
Dragan
I've been thinking about this some recently. Currently my
approach requires default ctors and then using a Receive
function to reset the data members. This approach is kind
of nice as it works when you want to use return codes.
My options are to add support for a stream ctor that is
only used when you are using exceptions. That seems like
a reasonable thing to do, but I don't think it completely
eliminates the need for the default ctor or Receive function.
Here's two examples:
(vector<Account>) // The message consists of a number of Account
// instances being sent/received.
In this case, the generated Receive function would look like this
void
Receive(Buffer* buf, vector<Account>& abt1)
{
// get number of elements in vector
for (int i = 0; i < headCount; ++i) {
Account acct(buf);
abt1.push_back(acct);
}
}
But if the message is just one Account instance --
(Account)
The Receive function will look like this:
void
Receive(Buffer* buf, Account& abt1)
{
abt1.Receive(buf);
}
So it seems to me that while there may be some
merit to having a stream ctor, the need for a
function very similar to the stream ctor, Receive,
may remain. On the other hand, those using return
codes can get away with just the single Receive
function. Perhaps placement new could be used to
eliminate the need for the Receive function in
the above example.
Brian Wood
Ebenezer Enterprises
www.webEbenezer.net
> Calling read in an improper place is impossible, because Device simply
> does not expose "read" interface. Now, this is done at the expense of
> introducing a new and possibly counterintuitive class. It assumes that
> those sessions do not overlap (otherwise it would not be possible to
> insert overlapping nested scopes). And, this is only a scenario I
> thought of. Perhaps the case you are thinking of is more compilcated
> than this.
This approach actually seems very intriguing, and once ocurred to me
while working with Java's Native Interface.
That API defines a JNIEnv resource which offers you the possibility
of calling functions on Java code, provided that you always
"attach" before such a call, and "detach" later.
In this case, your solution would be perfect if the DeviceSession
class would work like an "attach lock", making it virtually
impossible for the programmer to ever call Java functions in an
invalid situation.
> You list three sources of problems that I find very different:
>
> 1. The close operation.
> I would thinkk that additional "state" classes as in the above example
> could out-rule this problem.
I agree.
> 2. Move constructors.
> In C++0x (as far as I understand it) classes are moveable only if you
> make them moveable. If for your class move does not make sense or is
> dangerous, you just don't implement move constructors/assignments.
I don't actually think move constructors make it a requirement
on the class to support a "NULL" state. I think it just adds a little
to the list.
The contract of move constructors, as far as I know, is that the
object
must be left _destroyable_, i.e. calling the destructor on it must be
valid. The "NULL" state implies more than that.
> 3. Volatile invalidation.
> You are right. runtime checks cannot be avoided here.
>
> In the end, your point on volatile invalidation does prove that you
> cannot avoid the "closed" state. This is the kind of an example I was
> looking for.
Yes, this is where I think the problem lies.
For files, this problem doesn't exist in practice, because the file
system
implementation won't close files on your back.
But sockets and pipes can close unexpectedly.
--
P.
> The contract of move constructors, as far as I know, is that the
> object
> must be left _destroyable_, i.e. calling the destructor on it must be
> valid.
More is actually required by the standard library.
Assignment must work too, in both directions I think.
co...@mailvault.com schrieb:
> I've been thinking about this some recently. Currently my
> approach requires default ctors and then using a Receive
> function to reset the data members. This approach is kind
> of nice as it works when you want to use return codes.
My approach involves two classes. That is you have a data class which
does not have strict invariants. This data class can have this receive
function with or without exceptions. The data class is then used to
construct the "real" class instance.
> So it seems to me that while there may be some
> merit to having a stream ctor, the need for a
> function very similar to the stream ctor, Receive,
> may remain.
Yes. I think this function does not have to be a member of the target class.
Frank
Pedro Lamarão schrieb:
> For files, this problem doesn't exist in practice, because the file
> system
> implementation won't close files on your back.
>
> But sockets and pipes can close unexpectedly.
In this case you could also throw an exception.
struct Session {
Session(Device& d) { /*open*/ }
~Session() { /*close*/ }
std::string read() {
std::string data;
data = ...;
if(error)
throw std::runtime_error("...");
return data;
}
private:
...
};
This way you signal an error and avoid returning empty data (for example).
Frank
> > But sockets and pipes can close unexpectedly.
>
> In this case you could also throw an exception.
A socket or pipe getting closed unexpectedly is not necessarily an error.
It is normal for it to close once all information has been read.
Mathias Gaunard schrieb:
> A socket or pipe getting closed unexpectedly is not necessarily an error.
> It is normal for it to close once all information has been read.
You shouldn't be reading data from it then anymore. Reading (= calling
"read()") data from a closed socket is an error. If the socket object
cannot represent a closed socket (which was the point of the discussion)
then there is only the exception to be thrown, isn't there?
Frank
> You shouldn't be reading data from it then anymore. Reading (= calling
> "read()") data from a closed socket is an error. If the socket object
> cannot represent a closed socket (which was the point of the discussion)
> then there is only the exception to be thrown, isn't there?
Typically, one last read is allowed that will inform the user that the
socket has indeed been closed by returning empty data.
After that, yes, calling read is a programming error. I would even
make it UB and not bother throwing, personally.