[Boost-users] [multi_array] more on assignability

8 views
Skip to first unread message

Dr Johann A. Briffa

unread,
Jul 11, 2008, 6:17:04 AM7/11/08
to boost...@lists.boost.org
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

multi_array.h

Ronald Garcia

unread,
Jul 17, 2008, 3:19:47 PM7/17/08
to boost...@lists.boost.org
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 <boost/multi_array.hpp>
#include <boost/static_assert.hpp>
#include <boost/array.hpp>
#include <algorithm>

template <typename T, typename U, std::size_t N>
void
resize_from_MultiArray(boost::multi_array<T,N>& marray, U& other) {

// U must be a model of MultiArray
boost::function_requires<

boost
::detail::multi_array::ConstMultiArrayConcept<U,U::dimensionality> >();
// U must have U::dimensionality == N
BOOST_STATIC_ASSERT(U::dimensionality == N);

boost::array<typename boost::multi_array<T,N>::size_type, N> shape;

std::copy(other.shape(), other.shape()+N, shape.begin());

marray.resize(shape);

}

//
// Example usage
//

#include <iostream>


int main () {

boost::multi_array<int,2> A(boost::extents[5][4]), B;
boost::multi_array<int,3> C;

resize_from_MultiArray(B,A);

#if 0
resize_from_MultiArray(C,A); // Compile-time error
#endif

std::cout << B.shape()[0] << ", " << B.shape()[1] << '\n';

}

//
=
=
=
=
=
=
=
=
=
=
=
=
=
========================================================================

One can use this function to easily resize a multi_array in terms of
another multi_array. This does not solve the problem you describe
below, where you want operator=() to resize a multi_array.


>
> 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


_______________________________________________
Boost-users mailing list
Boost...@lists.boost.org
http://lists.boost.org/mailman/listinfo.cgi/boost-users

Daryle Walker

unread,
Jul 17, 2008, 10:05:41 PM7/17/08
to boost...@lists.boost.org, bo...@lists.boost.org
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

David Abrahams

unread,
Jul 17, 2008, 10:32:35 PM7/17/08
to bo...@lists.boost.org, boost...@lists.boost.org

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

Daryle Walker

unread,
Jul 18, 2008, 5:07:08 AM7/18/08
to boost...@lists.boost.org, bo...@lists.boost.org
On Jul 17, 2008, at 10:32 PM, David Abrahams wrote:

> on Thu Jul 17 2008, Daryle Walker <darylew-AT-hotmail.com> 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

_______________________________________________

Daryle Walker

unread,
Jul 18, 2008, 6:48:03 AM7/18/08
to bo...@lists.boost.org, boost...@lists.boost.org
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.

David Abrahams

unread,
Jul 18, 2008, 6:57:17 AM7/18/08
to bo...@lists.boost.org, boost...@lists.boost.org

on Fri Jul 18 2008, Daryle Walker <darylew-AT-hotmail.com> 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?

--
Dave Abrahams
BoostPro Computing
http://www.boostpro.com

Daryle Walker

unread,
Jul 18, 2008, 7:30:42 AM7/18/08
to bo...@lists.boost.org, boost...@lists.boost.org

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

_______________________________________________

David Abrahams

unread,
Jul 18, 2008, 8:03:00 AM7/18/08
to boost...@lists.boost.org, bo...@lists.boost.org, boost...@lists.boost.org

--
Dave Abrahams
Boostpro Computing
http://boostpro.com


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.

Daryle Walker

unread,
Jul 19, 2008, 10:47:57 PM7/19/08
to boost...@lists.boost.org, bo...@lists.boost.org
On Jul 18, 2008, at 8:03 AM, David Abrahams wrote:

> On Jul 18, 2008, at 7:30 AM, Daryle Walker <dar...@hotmail.com>
> 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

_______________________________________________

Reply all
Reply to author
Forward
0 new messages