Réponse de Pollux :


Olena's images classes, image1d<T>, image2d<T>, and image3d<T>, are really just proxies to the images data. Their use is seamless because they delegates all methods to the underlying class for the image data (these classes are internal::image1d_data<T>, etc.).

However, these classes behave like pointers vis-a-vis copying. For instance, after the following lines

1image2d<int_u8> img1("lena.pgm");
2image2d<int_u8> img2 = img1;

the two variables img1 and img2 refer to the same data and can be used interchangeably.

Each data object maintains a reference counter, so that it can be destroyed automatically when there is no more proxy pointing to it.

Using proxies allows efficient returns from procedures that create images, because we just return a pointer; without proxies, one would have to return the whole image by copy (very slow), or return a pointer and leave the memory management to the user.

(FIXME: It might be worth to document the other trick that earlier versions of Milena were using to speed up returns: the ret_image class.)

Anyway. So far so good: you can handle these proxies as if they were the real images, and copying them is as fast as copying a pointer. In the cases where you want to duplicate the image data, you can call img.clone().

One problem arises with the const qualifier. Everybody understands that const int* ptr1 is not the same as const int *const ptr2:

  • ptr1 is a pointer to constant data, the pointed data cannot be changed, but ptr1 itself can.
  • ptr2 is a constant pointer to constant data, neither the data nor the pointer can be modified.

Back to Olena, what does const image2d<T> mean? Our trouble is that image2d<T> behaves both like a pointer (during copy) and like the pointed data (all other operations). So should we interpret const image2d<T> as

  • a constant proxy that points to mutable data ?
  • a constant proxy that points to constant data ?

Because there is no other way to specify the constness of the underlying data, the current semantic of const image2d<T> is the latter: a constant proxy that points to constant data.

A corollary is that assignments (or copy constructions) from const image2d<T> to image2d<T> should be forbidden; otherwise this would allow to modify an image declared constant. Disabling these assignments is more tricky than it seems. Let's take a simple example.

 1struct Proxy
 3  Proxy () {}
 4  Proxy (Proxy&) {}
 5  // no constructor for `const Proxy&'.
11  Proxy p;
12  const Proxy q = p;				// OK (non-const -> const)
13  Proxy r = p;						// OK (non-const -> non-const)
14  // Proxy s = q;
15  //	no matching function for call to `Proxy::Proxy(const Proxy&)'
16  //	candidates are: Proxy::Proxy(Proxy&)

This seems to work fine.

However C++ forbids non-const references to rvalues. So we can't construct a proxy from the result of a function:

 ~/tmp % cat const2.cc 
 1struct Proxy
 3  Proxy () {}
 4  Proxy (Proxy&) {}
 7Proxy f()
 9  Proxy p;
10  return p;
16  Proxy p = f();
~/tmp % g++ const2.cc
const2.cc: In function `int main()':
const2.cc:16: initialization of non-const reference type `struct Proxy &'
const2.cc:16: from rvalue of type `Proxy'
const2.cc:4: in passing argument 1 of `Proxy::Proxy(Proxy &)'

One really needs a Proxy (const Proxy&) constructor to allow this sort of statements. However such a constructor would also allow these assignments for const Proxy to Proxy we try to prevent.

Declaring the const copy constructor as private is of no use, except to show that g++ tries to call it.

~/tmp % cat const2b.cc
 1struct Proxy
 3  Proxy () {}
 4  Proxy (Proxy&) {}
 6  Proxy (const Proxy&) {}
 9Proxy f()
11  Proxy p;
12  return p;
18  Proxy p = f();
~/tmp % g++ const2b.cc
const2b.cc: In function `int main()':
const2b.cc:6: `Proxy::Proxy(const Proxy &)' is private
const2b.cc:18: within this context

The workaround currently used in Olena is to declare this constructor as public, but without providing an implementation.

 ~/tmp % cat const3.cc 
 1struct Proxy
 3  Proxy () {}
 4  Proxy (Proxy&) {}
 5  Proxy (const Proxy&);			// no implementation
 8Proxy f()
10  Proxy p;
11  return p;
17  Proxy p = f();
 ~/tmp % g++ const3.cc 

At a first glance, it might be surprising that just declaring the constructor that g++ attempted to call is enough to get things working. Obviously this constructor is not really called otherwise we'd have a link failure.

The explanation is that the standard [section 12.8.15] allows the compiler to suppress the intermediate temporary variable (the result for f()), and have f() install it's result directly in =p=. Doing so suppress the call for Proxy (const Proxy&);.

GCC performs this optimization by default. See what happens if we disable it:

~/tmp % g++ -fno-elide-constructors const3.cc
/tmp/ccpLq07h.o: In function `main':
/tmp/ccpLq07h.o(.text+0x59): undefined reference to `Proxy::Proxy(Proxy const &)'
collect2: ld returned 1 exit status

Granted, relying on an optimization is not very cunning. But wait, it's not the sole issue :)

Some standard tools do assume that copy-constructing a non-const object from a const object is legitimate.

 ~/tmp % cat const4.cc 
 1#include <list>
 3struct Proxy
 5  Proxy () {}
 6  Proxy (Proxy&) {}
 7  Proxy (const Proxy&);			// no implementation
13  std::list<Proxy> l;
14  Proxy p;
15  l.push_front(p);
~/tmp % g++ const4.cc
/tmp/cctdFmoL.o: In function `void construct<Proxy, Proxy>(Proxy *, Proxy const &)':
/tmp/cctdFmoL.o(.void gnu.linkonce.t.construct<Proxy, Proxy>(Proxy *, 
Proxy const &)+0x2d): undefined reference to `Proxy::Proxy(Proxy const &)'
collect2: ld returned 1 exit status

The construct function looks as follows

1template <class T1, class T2>
2inline void construct(T1* p, const T2& value) {
3  new (p) T1(value);

It just builds a copy of value at address p=. Since =value is not modified by construct it is declared as a constant reference. So this new operator calls the const version of our copy constructor, hence the failure.

Warning: Display title "A solution?" overrides earlier display title "The problem".

In my opinion, the problem is the choice that const image2d<T> means a constant proxy that points to constant data. This is odd, because const applies on two objects (the image, and the data), unlike all other uses of const in the language.

If the semantic of const image2d<T> was changed to a constant proxy that points to mutable data, we'd need another mean to indicate that the data are constant.

This could be done with a new class, const_image2d<T>, just like the C++ standard has iterator and const_iterator.

A function that takes a constant image (constant proxy + constant data) as argument would have to be written as follows.

1void foo(const const_image<T>& img)
3  ...

Since we want to be able to pass arguments like image2d<T>, it means that const_image<T> should be a base class of image2d<T>. Similarly, const_image2d<T> should be a base class of image2d<T>; but image<T> should not be a base class for const_image2d<T>. This suggests a complex hierarchy which might not be easy to implement.

Here is how it would look like for 1D and 2D images:

{| class="wikitable"

{| class="wikitable"
					 /	||	\					  

{| class="wikitable"
					/	 ||	 \					 

{| class="wikitable"
				  /	image	\					

{| class="wikitable"
				 /	  / \	  \				  

{| class="wikitable"
				/	  /	\	  \				 

{| class="wikitable"
const_image1d	 /	  \	 const_image2d 

{| class="wikitable"
			 \	  /		 \	  /			  

{| class="wikitable"
			  \	/			\	/				

{| class="wikitable"
			 image1d		 image2d			  

The obvious issue is that there is two ways to convert image2d to const_image. So calling the foo function above with an image2d requires an explicit cast to either image or const_image2d first...

Warning: Display title "Contribution de Benoit Autheman" overrides earlier display title "A solution?".

J'avais lu il y a un moment ta doc sur le probleme de 'constness' dans Olena sur ConstImages.

Je suis actuellement en train de modifier std::auto_ptr pour travailler avec des references, et la technique employée dans le code de la stl pour empecher l'utilisation du copy constructor m'a semble appliquable à ton probleme.

Globalement, le trick consiste à definir un constructeur dans Proxy qui est initialise avec une instance d'une autre classe (ici Proxy::Ref), et à definir un operateur de cast non const de Proxy vers Proxy::Ref dans Proxy. Cette methode s'applique generalement lorsque aucun copy constructor n'a ete defini dans Proxy : Globalement cela empeche de pouvoir initialiser un Proxy non const a partir d'un Proxy const. Ma (modeste;) modification consiste à définir effectivement le copy constructor et à le specifier explicit. On a donc les avantages des 2 techniques : (Meme classes que dans ta doc) Ce code compile comme voulu:

1		  Proxy p;
2		  const Proxy q = p;		// OK (non-const -> const)
3		  Proxy r = p;				// OK (non-const -> non-const)
4//		Proxy s = q;				// Compilation error if uncommented

Par contre cela aussi compile:

1		  Proxy proxy( )  { return Proxy( ); }
2		  ...
3		  // Ton test 2,
4		  Proxy	pb( proxy( ) );
5		  pb.nimportequoi( );

Et plus interressant, ca aussi ca compile !:

1		  // Ton test 3
2		  list< Proxy > l;
3		  l.push_front( p );

Bien sur, ce n'est pas l'ideal, et normalement quelque chose dans ce style devrais marcher (imho?) mais ne compile pas malheureusement:

1		  const Proxy pa;
2		  const Proxy pb = pa;

Mon explication est certainement pas tres claire, mais c'est loin d'etre trivial au final, meme si cela tient en peu de lignes de code.

Le .cpp ci-joint compile avec gcc 3.0.4 et est inspiré fortement du source de auto_ptr dans le header std_memory.h. (J'espere que toutes les versions de gcc appliquent les memes regles de prioritee vis a vis des cast implicites, sans quoi cela risque de ne pas fonctionner)

C'est un hack fait rapidement (relativement), donc je ne pretend pas que ca marche, c'est très peu testé (voire pire ;) et je suis peut etre passe completement a cote du probleme. Ceci dit, ca t'interressera peut etre vu que les exemples que tu donnes compilent avec cette technique...

a+ Benoit

ps: Le trick d' auto_ptr: http://www.langer.camelot.de/Articles/C++Report/AutoPointer/AutoPointer.html pps: Compile avec g++ 3.0.4:

g++ -fno-elide-constructors olenaconstimages.cpp
 1#include <string>
 2#include <list>
 4class Proxy
 6  struct Ref
 7  {
 8	 Ref(Proxy& p) : _p(p) {}
 9	 ~Ref() {}
10	 Proxy&	_p;
11  };
14  Proxy() : _s( "default" ) {}
15  Proxy(string s) : _s(s) {}
17  // With no 'explicit' keyword and a 'Ref' convertion operator defined,
18  // the precendence  of this ctor became more important than the default
19  // copy ctor one.
20  Proxy(Ref e) : _s(e._p._s) {}
21  Proxy(Proxy& p) : _s(p._s) {}
23  // The copy ctor must be declared explicit
24  explicit Proxy(const Proxy& p) : _s(p._s) {}
25  ~Proxy() {}
27  // Cast en ProxyRef
28  operator Ref() {return Ref(*this);}
31  string _s;
34Proxy f(const char* t) { return Proxy(t); }
36int main( )
38  Proxy p("p");
39  const Proxy q = p;	// OK (non-const -> const)
40  Proxy r = p;		// OK (non-const -> non-const)
41  // Proxy s = q;		 // Compilation error cool (no const ref copy ctor)
43  Proxy	p2 = f("test");
45  std::list< Proxy > l;
46  l.push_front(p);		// Compilation ok
48  // Do not work:
49  // const Proxy pc("pc");
50  // const Proxy pcb = pc;

Warning: Display title "Réponse de Pollux :" overrides earlier display title "Contribution de Benoit Autheman".

Benoit> Bien sur, ce n'est pas l'ideal, et normalement quelque
Benoit> chose dans ce style devrais marcher (imho?) mais ne
Benoit> compile pas malheureusement:

Benoit> const Proxy pa;
Benoit> const Proxy pb = pa;

Je ne comprend pas pourquoi c'est rejeté. En particulier, quelle différence y-a-t-il entre

1const Proxy pa;
2const Proxy pb (pa);


1const Proxy pa;
2const Proxy pb = pa;


J'avais toujours cru que ces syntaxes étaient équivalantes, or gcc compile la première mais pas la seconde. Comeau C++ itou.

Une idée ?


Benoit> (J'espere que toutes les versions de gcc appliquent les
Benoit> memes regles de prioritee vis a vis des cast
Benoit> implicites, sans quoi cela risque de ne pas
Benoit> fonctionner)

La compilation échoue avec g++-2.95 -fno-elide-constructors, mais ce n'est pas inquiétant puisque ça compile avec les options par défaults, ansi qu'avec d'autres compilateurs que gcc. (Dans mon exemple je n'utilisais -fno-elide-constructors que pour obtenir de gcc les même diagnostics que d'autres compileurs.)

adl> quelle différence y-a-t-il entre

adl> const Proxy pa;
adl> const Proxy pb (pa);

adl> et

adl> const Proxy pa;
adl> const Proxy pb = pa;

adl> ?

D'après la norme, la première syntaxe est un appel de constructeur explicite, tandis que la seconde est une conversion implicite (§12.3.1/2 et §12.3.1/3). Cette dernière est donc interdite avec les constructeurs explicites.