[multi_array] more on assignability
I realize this topic has been discussed before (I've seen at least a
thread in 2006 and another in 2007), but I believe there is more to add.
At issue is the decision that multi_array objects cannot be assigned
[with the operator=() that is] unless the target is already the same
size as the source. Further, there is no documented way to easily resize
an existing array to the same size and range as some other array.
Let me start with the first issue: others have already said before that
making multi_array assignable would be a good thing. Ron disagrees on
this point, and from what I gather his principal reason is that
multi_array should have the same semantics as multi_array_ref etc.
[please do correct me if I'm wrong here, I've only had a day to sort
this out]. I am unsure why multi_array_ref etc cannot also have such an
interface, but then I haven't used that class enough so my knowledge is
very limited there.
As far as my needs are concerned, a multi_array that cannot be assigned
is next to useless. Allow me to elaborate: I am using multi_array as a
replacement to my own (old) 2/3D containers, as recently I've had need
for higher dimensions. I have classes containing array members, where
the size of the arrays is dynamic through the object lifetime. So my
class default constructor uses the array default constructor for its
member objects, creating empty arrays. Eventually as my classes get
used, the array members are updated accordingly, and get to have some
non-zero size. Now the problem is that my classes need to be assignable,
and the only way that can be achieved is if the array members are
assignable; the alternative is to have to write a specific assignment
operator for any of my classes that use arrays, resizing each member of
the target before copying. Clearly that would involve a lot of extra effort.
The second (admittedly minor) problem is that the only way to resize the
target is unclean and the semantics are bound to the dimensionality:
target.resize(boost::extents[source.shape()[0]][source.shape()[1]]);
etc. I would rather have something like:
target.resize(source.extents());
Ron had mentioned in 2007 that the lack of such an interface was an
oversight, but there is still no solution.
I am bringing up these issues again because:
1) I am not convinced there is any valid reason not to have multi_array
assignable. I do not buy the performance issue, as this would only kick
in if (a) assignment is within a performance-sensitive path and (b) the
target is not already of the same size. Further, at the very least there
could be a derived class (somewhat like what I document below) that
allows such assignment.
2) It would be nice to have the clean resize interface as part of the
multi_array class.
As an interim solution for myself, I derived a new class from
multi_array that allows assignment and does the resize automagically. I
am copying the code below for public benefit. Notes:
i) anyone is free to (re)use this code under the same license as
multi_array; in particular, Ron, please do feel free to adapt and reuse
within a future multi_array as you see fit.
ii) while the interface provided is sufficient for the way I use the
class, it certainly needs to be adapted to be completely generic.
Johann
#ifndef __multi_array_h
#define __multi_array_h
#include "config.h"
#include
Hello Johann, On Jul 11, 2008, at 6:17 AM, Dr Johann A. Briffa wrote:
I realize this topic has been discussed before (I've seen at least a thread in 2006 and another in 2007), but I believe there is more to add. At issue is the decision that multi_array objects cannot be assigned [with the operator=() that is] unless the target is already the same size as the source. Further, there is no documented way to easily resize an existing array to the same size and range as some other array.
Indeed there is no built-in way to easily resize a multi_array to be
the same size as another multi_array. While there is no resize member
function that takes a pointer, there is one that takes a model of the
Collection concept (classes like boost::array and std::vector are
concepts). Based on this interface, here is a stop-gap non-invasive
solution that can resize a multi_array based on another model of
MultiArray (multi_array, multi_array_ref, etc.):
//
=
=
=
=
=
=
=
=
=
=
========================================================================
#include
As far as my needs are concerned, a multi_array that cannot be assigned is next to useless. Allow me to elaborate: I am using multi_array as a replacement to my own (old) 2/3D containers, as recently I've had need for higher dimensions. I have classes containing array members, where the size of the arrays is dynamic through the object lifetime. So my class default constructor uses the array default constructor for its member objects, creating empty arrays. Eventually as my classes get used, the array members are updated accordingly, and get to have some non-zero size. Now the problem is that my classes need to be assignable, and the only way that can be achieved is if the array members are assignable; the alternative is to have to write a specific assignment operator for any of my classes that use arrays, resizing each member of the target before copying. Clearly that would involve a lot of extra effort.
I believe that in a previous discussion I had suggested a template argument-based solution to this, where a multi_array type would have resizing assignment (and thereby no longer be a model of the MultiArray concept). I don't recall getting feedback on that proposal. Cheers, ron
On Jul 17, 2008, at 3:19 PM, Ronald Garcia wrote:
On Jul 11, 2008, at 6:17 AM, Dr Johann A. Briffa wrote:
I realize this topic has been discussed before (I've seen at least a thread in 2006 and another in 2007), but I believe there is more to add. At issue is the decision that multi_array objects cannot be assigned [with the operator=() that is] unless the target is already the same size as the source. Further, there is no documented way to easily resize an existing array to the same size and range as some other array.
Indeed there is no built-in way to easily resize a multi_array to be the same size as another multi_array. While there is no resize member function that takes a pointer, there is one that takes a model of the Collection concept (classes like boost::array and std::vector are concepts). Based on this interface, here is a stop- gap non-invasive solution that can resize a multi_array based on another model of MultiArray (multi_array, multi_array_ref, etc.): [TRUNCATE]
Let's define some stuff here: T: some type you created, probably a class type a: an object of type T; it's valid, i.e. meets its invariants b: another object of type T, also valid; any similarities or differences in its state from a's state is unspecified C: the set of all valid states an object of type T may have Let function F: x -> Y, map a T object state x to a (sub)set of C named Y. This function returns the subset of source states that a given state can receive during an assignment. If there exists at least one x in C such that F(x) = Y < C, then type T is _NOT_ Assignable! In other words, the number of assignment- compatibility classes in T must be exactly one for T to be Assignable. Furthermore, an Assignable type requires that the destination object's observable state post-assignment matches the observable state of the source object. The destination's new state can't be the old state quasi-merged with the source's state. (The destination object's old state must appear to be splattered, but remnants could be cached to the new state.) Fortunately, most types are Assignable. What happens if your type isn't? 1. Users can't use objects of that type in standard containers, which assume to be able to freely copy objects as much as they want. 2. Such objects are a pain to work with. Any containing class must write custom assignment operators to add calls to your custom copying routine. Of course, they have to first check if the source and destination objects are in the same assignment-compatibility class, and reconfigure at least one of those two objects if they're not in the same class. The exception would be if the wrapping class always makes sure that every T object it creates is of the same class. Even that is a pain because the user has to manually confirm that invariant every time s/he changes or adds a mutating function. 3. It isn't safe. You are effectively dumping the responsibility of assignment onto your users, who won't have all the information. The user generally has to test compatibility, reconfigure at least one object, then do call the custom copying routine. These steps are distinct, so if one of the mutating steps throws, the user will probably lose his/her old state forever and have a default-valued object. Your assignment routine would have more information available, so it can structure the assignment with roll-back during throws. (This could be moderated with a swap routine that doesn't care about assignment-compatibility classes. You do have a custom swap, right? And it does work irrespective of class, right?!) -- Daryle Walker Mac, Internet, and Video Game Junkie darylew AT hotmail DOT com
on Thu Jul 17 2008, Daryle Walker
On Jul 17, 2008, at 3:19 PM, Ronald Garcia wrote:
On Jul 11, 2008, at 6:17 AM, Dr Johann A. Briffa wrote:
I realize this topic has been discussed before (I've seen at least a thread in 2006 and another in 2007), but I believe there is more to add. At issue is the decision that multi_array objects cannot be assigned [with the operator=() that is] unless the target is already the same size as the source. Further, there is no documented way to easily resize an existing array to the same size and range as some other array.
Indeed there is no built-in way to easily resize a multi_array to be the same size as another multi_array. While there is no resize member function that takes a pointer, there is one that takes a model of the Collection concept (classes like boost::array and std::vector are concepts). Based on this interface, here is a stop- gap non-invasive solution that can resize a multi_array based on another model of MultiArray (multi_array, multi_array_ref, etc.): [TRUNCATE]
Let's define some stuff here: T: some type you created, probably a class type a: an object of type T; it's valid, i.e. meets its invariants b: another object of type T, also valid; any similarities or differences in its state from a's state is unspecified C: the set of all valid states an object of type T may have
Let function F: x -> Y, map a T object state x to a (sub)set of C named Y. This function returns the subset of source states that a given state can receive during an assignment.
If there exists at least one x in C such that F(x) = Y < C, then type T is _NOT_ Assignable! In other words, the number of assignment- compatibility classes in T must be exactly one for T to be Assignable.
Then, if I understand you correctly, none of the built-in types are Assignable. char* p; // p is unintialized char* q = p; // invalid Yes, uninitialized is one of the valid states for a builtin type, i.e. part of the type's invariants. -- Dave Abrahams BoostPro Computing http://www.boostpro.com
On Jul 17, 2008, at 10:32 PM, David Abrahams wrote:
on Thu Jul 17 2008, Daryle Walker
wrote: [SNIP]
Let's define some stuff here: T: some type you created, probably a class type a: an object of type T; it's valid, i.e. meets its invariants b: another object of type T, also valid; any similarities or differences in its state from a's state is unspecified C: the set of all valid states an object of type T may have
Let function F: x -> Y, map a T object state x to a (sub)set of C named Y. This function returns the subset of source states that a given state can receive during an assignment.
If there exists at least one x in C such that F(x) = Y < C, then type T is _NOT_ Assignable! In other words, the number of assignment- compatibility classes in T must be exactly one for T to be Assignable.
Then, if I understand you correctly, none of the built-in types are Assignable.
char* p; // p is unintialized char* q = p; // invalid
Yes, uninitialized is one of the valid states for a builtin type, i.e. part of the type's invariants.
Really, I was wondering about that (corner) case, especially since it can't be replicated (i.e. it's undefined to use such a state as a source). I'm thinking more about non-POD class types, which must have an initial state with the internal primitive objects initialized. -- Daryle Walker Mac, Internet, and Video Game Junkie darylew AT hotmail DOT com
On Jul 18, 2008, at 5:07 AM, Daryle Walker wrote:
On Jul 17, 2008, at 10:32 PM, David Abrahams wrote:
[SNIP]
Then, if I understand you correctly, none of the built-in types are Assignable.
char* p; // p is unintialized char* q = p; // invalid
Yes, uninitialized is one of the valid states for a builtin type, i.e. part of the type's invariants.
Really, I was wondering about that (corner) case, especially since it can't be replicated (i.e. it's undefined to use such a state as a source). I'm thinking more about non-POD class types, which must have an initial state with the internal primitive objects initialized.
Well, I looked into it further. In C++ 2003, section 4.1 "Lvalue-to- rvalue conversion" [conv.lval], paragraph 1, an uninitialized object can only be used as an lvalue, converting it to a rvalue is undefined behavior. This means that your program is illegitimate and we can't count it as a counter-example. -- Daryle Walker Mac, Internet, and Video Game Junkie darylew AT hotmail DOT com
on Fri Jul 18 2008, Daryle Walker
On Jul 18, 2008, at 5:07 AM, Daryle Walker wrote:
On Jul 17, 2008, at 10:32 PM, David Abrahams wrote:
[SNIP]
Then, if I understand you correctly, none of the built-in types are Assignable.
char* p; // p is unintialized char* q = p; // invalid
Yes, uninitialized is one of the valid states for a builtin type, i.e. part of the type's invariants.
Really, I was wondering about that (corner) case, especially since it can't be replicated (i.e. it's undefined to use such a state as a source). I'm thinking more about non-POD class types, which must have an initial state with the internal primitive objects initialized.
Well, I looked into it further. In C++ 2003, section 4.1 "Lvalue-to- rvalue conversion" [conv.lval], paragraph 1, an uninitialized object can only be used as an lvalue, converting it to a rvalue is undefined behavior.
Yes, that's what "// invalid" means.
This means that your program is illegitimate and we can't count it as a counter-example.
Huh? By that logic no counterexample is possible. Or am I missing something? -- Dave Abrahams BoostPro Computing http://www.boostpro.com
On Jul 18, 2008, at 6:57 AM, David Abrahams wrote:
on Fri Jul 18 2008, Daryle Walker
wrote: On Jul 18, 2008, at 5:07 AM, Daryle Walker wrote:
On Jul 17, 2008, at 10:32 PM, David Abrahams wrote:
[SNIP]
Then, if I understand you correctly, none of the built-in types are Assignable.
char* p; // p is unintialized char* q = p; // invalid
Yes, uninitialized is one of the valid states for a builtin type, i.e. part of the type's invariants.
Really, I was wondering about that (corner) case, especially since it can't be replicated (i.e. it's undefined to use such a state as a source). I'm thinking more about non-POD class types, which must have an initial state with the internal primitive objects initialized.
Well, I looked into it further. In C++ 2003, section 4.1 "Lvalue-to- rvalue conversion" [conv.lval], paragraph 1, an uninitialized object can only be used as an lvalue, converting it to a rvalue is undefined behavior.
Yes, that's what "// invalid" means.
This means that your program is illegitimate and we can't count it as a counter-example.
Huh? By that logic no counterexample is possible. Or am I missing something?
Yes, the OP was having a problem with a boost class (template) that is like std::valarray: you can only do assignments if the source and destination objects _already_ had the same layout. And my first response to this thread explained why this is problematic for our users, and so we shouldn't do this. In this setup, all objects are validly initialized; it's just there isn't a single assignment- compatibility class that all valid object states belong to. Note that you have to go out of your way to create a class like this. It's a pain because the user who wants to use this class internally has either make sure all wrapping objects keep all sub-objects of this type within the same assignment-compatibility class or write a custom assignment routine. Said routine can be at most the basic guarantee if the resizing or copy steps may throw. (A strong guarantee could be done if the type provides a swap that can cross assignment-compatibility classes.) An author-supplied full- assignment routine could take advantage of the internal implementation and add rollback. -- Daryle Walker Mac, Internet, and Video Game Junkie darylew AT hotmail DOT com
--
Dave Abrahams
Boostpro Computing
http://boostpro.com
On Jul 18, 2008, at 7:30 AM, Daryle Walker
On Jul 18, 2008, at 6:57 AM, David Abrahams wrote:
on Fri Jul 18 2008, Daryle Walker
wrote: On Jul 18, 2008, at 5:07 AM, Daryle Walker wrote:
On Jul 17, 2008, at 10:32 PM, David Abrahams wrote:
[SNIP]
Then, if I understand you correctly, none of the built-in types are Assignable.
char* p; // p is unintialized char* q = p; // invalid
Yes, uninitialized is one of the valid states for a builtin type, i.e. part of the type's invariants.
Really, I was wondering about that (corner) case, especially since it can't be replicated (i.e. it's undefined to use such a state as a source). I'm thinking more about non-POD class types, which must have an initial state with the internal primitive objects initialized.
Well, I looked into it further. In C++ 2003, section 4.1 "Lvalue- to- rvalue conversion" [conv.lval], paragraph 1, an uninitialized object can only be used as an lvalue, converting it to a rvalue is undefined behavior.
Yes, that's what "// invalid" means.
This means that your program is illegitimate and we can't count it as a counter-example.
Huh? By that logic no counterexample is possible. Or am I missing something?
Yes, the OP was having a problem with a boost class (template) that is like std::valarray: you can only do assignments if the source and destination objects _already_ had the same layout.
I understand that
And my first response to this thread explained why this is problematic for our users,
I understand that too. My problem is with your implicit declaration that such types are not to be considered Assignable. That concept is supposed to be compatible with the builtins.
and so we shouldn't do this. In this setup, all objects are validly initialized; it's just there isn't a single assignment-compatibility class that all valid object states belong to. Note that you have to go out of your way to create a class like this.
Not really. It's pretty easy to end up with uninitialized members. But that's really beside the point.
It's a pain because the user who wants to use this class internally has either make sure all wrapping objects keep all sub-objects of this type within the same assignment-compatibility class or write a custom assignment routine. Said routine can be at most the basic guarantee if the resizing or copy steps may throw.
So? I don't see how that's related or why it's a problem
(A strong guarantee could be done if the type provides a swap that can cross assignment-compatibility classes.) An author-supplied full-assignment routine could take advantage of the internal implementation and add rollback.
On Jul 18, 2008, at 8:03 AM, David Abrahams wrote:
On Jul 18, 2008, at 7:30 AM, Daryle Walker
wrote: [SNIP]
Yes, the OP was having a problem with a boost class (template) that is like std::valarray: you can only do assignments if the source and destination objects _already_ had the same layout.
I understand that
And my first response to this thread explained why this is problematic for our users,
I understand that too. My problem is with your implicit declaration that such types are not to be considered Assignable. That concept is supposed to be compatible with the builtins.
My use of "Assignable" was more colloquial, and not one of the official meanings in various C++ literature. What does practical assignability mean? It means that any object of a certain type can be either initialized by or receive an assignment from any other valid object of that type (Uninitialized PODs can't count because a program that uses one as an r-value is illegal.) and the destination object becomes observationally identical to the source object. If an object only lets certain objects of the same type be allowed to be assignment-source objects, then its utility becomes a lot lower because the user has to add more checks on his/her code. It might be OK for a mixed-operation-assignment to discriminate (with either an assertion or exception) because there's no way to match two objects with incompatible parameters for the desired operation. But the regular copy-assignment is different. It has a special place in the C++ type/object model, so much so that said operator is automatically defined if it's not explicitly provided. Anything that messes up the assumptions that C++ has about regular assignment, like multiple assignability classes or non-splatter semantics, cascades until an ugliness-hiding wrapper is made.
and so we shouldn't do this. In this setup, all objects are validly initialized; it's just there isn't a single assignment- compatibility class that all valid object states belong to. Note that you have to go out of your way to create a class like this.
Not really. It's pretty easy to end up with uninitialized members. But that's really beside the point.
It's a pain because the user who wants to use this class internally has either make sure all wrapping objects keep all sub- objects of this type within the same assignment-compatibility class or write a custom assignment routine. Said routine can be at most the basic guarantee if the resizing or copy steps may throw.
So? I don't see how that's related or why it's a problem
It's an example of the hoops a user has to do if they need to "correct" the assignment on their end. The problem is that the user's code doesn't have access to the inside information needed to increase the safety guarantee's strength. If the author provided the assignment, s/he could have used the inside information to strengthen the safety guarantee.
(A strong guarantee could be done if the type provides a swap that can cross assignment-compatibility classes.) An author- supplied full-assignment routine could take advantage of the internal implementation and add rollback.
-- Daryle Walker Mac, Internet, and Video Game Junkie darylew AT hotmail DOT com
participants (4)
-
Daryle Walker
-
David Abrahams
-
Dr Johann A. Briffa
-
Ronald Garcia