DGtal  1.4.beta
Parameter passing, cloning and referencing
Author(s) of this documentation:
Jacques-Olivier Lachaud

Part of the Base package.

This module gathers classes to make easier and more readable parameter passing by expliciting its type in the signature of functions and methods. The main classes are Clone, Alias and ConstAlias. They can be used in conjunction with smart pointer classes CountedPtr, CowPtr, CountedPtrOrPtr, CountedConstPtrOrConstPtr. These classes do not add a significant slowdown (between 0 and 10%) in elementary cases, while they can save 50% of time in some cases (with lazy duplication and move).

Related tests are testCloneAndAliases.cpp, testClone2.cpp.

Categories of parameter passing

In the following, the programmer is the one that has written the function/method f being called, while the user is the one that has written the code that calls f.

Parameter passing in C++

From a C++ point of view, you may pass an object of type T as parameter to f through the following mechanism:

  • by value with f(T t ). It is mostly used when passing native types (like int, bool, etc), small objects like iterators, as input. It indicates to the user that its argument is duplicated (at least once) and not modified. The user may also give a right-value object as parameter, though without move possibility.
  • by const reference with f(const T& t ). This form is generally used as an alternative to passing-by-value, to pass bigger objects as input. It may sometimes indicate to the user that the argument is const referenced in f for further uses, especially when f is a constructor/method of another object. It is unclear whether the lifetime of the argument should exceed the lifetime of f . This fact is clarified generally by the documentation.
  • by const pointer with f(const T* ptrT ). This form is sometimes used as an alternative to passing-by-value, to pass bigger objects as input while authorizing an invalid object (null pointer). Although this form is more C-like, it may be used in some C++ situation (like creating or destroying relations between objects). It may sometimes indicate to the user that the argument is const pointed in f for further uses, especially when f is a constructor/method of another object. It is unclear whether the lifetime of the argument should exceed the lifetime of f . This fact is clarified generally by the documentation.
  • by reference with f(T& t ). It is generally used for modifying an object (input-output or output). It may sometimes indicate to the user that the argument is referenced in f for further uses and modifications, especially when f is a constructor/method of an object. It is unclear whether the lifetime of the argument should exceed the lifetime of f . This fact is clarified generally by the documentation.
  • by pointer with f( T* ptrT ). This form is sometimes used as an alternative to passing-by-reference for modifying an object (input-output or output). Although this form is more C-like, it may be used also for specifying acquisition of the object (which should then have been dynamically allocated). It may sometimes indicate to the user that the argument is pointed in f for further uses and modifications, especially when f is a constructor/method of an object. It is unclear whether the lifetime of the argument should exceed the lifetime of f . This fact is clarified generally by the documentation.

The preceding discussion shows that standard parameter passing is ambiguous by nature, since each parameter passing method has several meanings. The user is forced to checked carefully the documentation (at best) or the full code to see how parameters are used.

Alternative definition for parameter passing

The programmer should explicit its intent to the user in an unambiguous way directly in the signature of f. We propose the following taxonomy:

  1. input parameters
    1. immediate use. The argument is used immediately in f, it is not pointed nor referenced for further use, and its lifetime is whatever.
    2. immediate cloning. The argument is cloned in f for further secure use or modifications, and hence its lifetime is whatever.
    3. long-term const aliasing. The argument is aliased in f for further use. The user is warned that he must adapt the lifetime of the argument consequently (generally the lifetime of the other object). The user knows that its argument will not be modified.
    4. object transfer or pointer acquisition. The argument was dynamically allocated and you wish to transfer its ownership to another object through call of f. This is a cloning for the programmer induced by a specific call by the user with a pointer.
  2. input-output and output parameters
    1. immediate use. The argument is used immediately in f, it is not pointed or referenced for further use, and its lifetime is whatever. The argument is modified by f.
    2. long-term aliasing (or sharing). The argument is aliased in f for further use and modifications. The user is warned that he must adapt the lifetime of the argument consequently (generally the lifetime of the other object). The user knows that its argument is shared by another object.

Parameter passing with Clone, Alias and ConstAlias

Classes Clone, Alias, and ConstAlias are defined in order to accept several arguments from the user. Symmetrically, they may be automatically converted into several types for the programmer. Not all combinations are allowed, but several optimizations are made, most notably by avoiding duplication if possible, by performing lazy duplication, or by moving objects. Furthermore, even for long-term aliasing, the user may choose at compile time between either an unsecure or secure aliasing method, depending on its application.

Therefore, those three classes are useful both from an efficiency and pragmatic point of view.

Disambiguating parameter passing

We propose the following ways to pass parameters without ambiguity:

input parameter immediate use immediate cloning / object transfer long-term const-aliasing
basic types, small object f( T t ) f( Clone<T> t ) f( ConstAlias<T> t )
bigger objects f( const T& t ) f( Clone<T> t ) f( ConstAlias<T> t )
User must be careful of argument lifetime no no yes
input-output / output parameter immediate use long-term aliasing / sharing
any type/object f( T& t ) f( Alias<T> t )
User must be careful of argument lifetime no yes
Note
Parameter passing by pointer may be used in place of references in the first column, altough this is not encouraged. Indeed, you have to think about your parameter passing by pointer to determine whether it is truely an immediate use or an implicit Clone<T*>. You may use a parameter passing by pointer in the following cases:
  • you wish to alias / const-alias an object that may be null (0) in some cases, like in an optional object association. In this case, you should use a pointer member.
  • you are manipulating arrays: pointers relate to cells of your array and are in fact iterators passed by value. Perhaps consider a typedef to remove ambiguity.

User passing an argument to a Clone parameter

A parameter ( Clone<T> cT ) accepts several types of arguments constructed from T. The true behavior of Clone depends also on the way the programmer uses the object cT in the function/method. The object cT may be used to initialize 3 different types of object (often a data member): T, T*, CowPtr<T>. The following table sums up the different conversions Argument toward member.

Argument type: const T& T* CountedPtr<T> CowPtr<T> T&& (c++11)
To:T Dupl. O(N) Acq. Dupl. O(N) Dupl. O(N) Dupl. O(N) Move. O(1)
To:T* Dupl. O(N) Acq. O(1) Dupl. O(N) Dupl. O(N) Move. O(1)
To:CowPtr<T> Dupl. O(N) Acq. O(1) Lazy. O(1)/O(N) Lazy. O(1)/O(N) Move. O(1)

where O(N) stands for the time to duplicate an instance of T, and with the following abbreviations:

  • Dupl. Object is duplicated.
  • Lazy. Object is lazily duplicated, meaning only when the user writes on it (which may be never).
  • Acq. Dynamically allocated pointer is acquired. User should take care himself of deletion only if storing the parameter with a pointer.
  • Move. The object is moved into the new object. This is generally much faster than copy. This is true for instance for all classical STL containers. You can also write a specific move constructor for your class.

It is clear that worst case is duplication while sometimes Clone is constant time (while guaranteeing object invariance and life-time).

Note
A conversion to T* means pointer acquisition. Hence the programmer should take care of deletion of the object. Otherwise, deletion is automatic for T or CowPtr<T> member. Furthermore, a conversion to a T* requires the use of the address operator (operator&) by the developer. If argument is Clone<T> a, and member name is b:
member type: T T* CowPtr<T>
member deletion: automatic manual automatic
conversion: automatic: b(a) address: b(&a) automatic: b(a)
Note
When choosing a Clone<T> parameter, the programmer should really consider using a CowPtr<T> member to store it, since it is the most versatile and optimizable variant. The only advantage of the two others storing methods (T and T*) is that there is one less indirection.
struct BigFatA { ... };
struct B {
// versatile constructor. Accepts 5 different versions.
B( Clone<BigFatA> a ) : myOwnFat( a ) {}
void f() { myOwnFat->do(); }
CowPtr<BigFatA> myOwnFat;
};
BigFatA a1;
B b1( a1 ); // cloned
B b2( new BigFatA( "I am the one " ) ); // acquired
CountedPtr<BigFatA> counted_ptr = new BigFatA( "I am the other one" );
B b3( counted_ptr ); // O(1) since lazy duplication (waiting for a write).
B b4( BigFatA( "Moving one" ) ); // this one is created directly in b4 (with c++11).

This version acquires the given argument. Note the use of the address operator operator& and the delete in the destructor.

struct BigFatA { ... };
struct B {
// versatile constructor. Accepts 5 different versions.
B( Clone<BigFatA> a ) : myAcquiredFat( &a ) {} // note the address operator
~B() { delete myAcquiredFat; } // required because of pointer acquisition
void f() { myOwnFat->do(); }
BigFatA* myAcquiredFat;
};
BigFatA a1;
B b1( a1 ); // cloned
B b2( new BigFatA( "I am the one " ) ); // acquired
CountedPtr<BigFatA> counted_ptr = new BigFatA( "I am the other one" );
B b3( counted_ptr ); // duplicated
B b4( BigFatA( "Moving one" ) ); // this one is created directly in b4 (with c++11).

User passing an argument to an Alias parameter

A parameter ( Alias<T> cT ) accepts several types of arguments constructed from T. The true behavior of Alias depends also on the way the programmer uses the object cT in the function/method. The object cT may be used to initialize 3 different types of object (often a data member): T&, T*, CountedPtrOrPtr<T>. The following table sums up the different conversions Argument toward member.

Argument type T& T* CountedPtr<T> CountedPtrOrPtr<T>
To: T& Shared. O(1) Shared. O(1)
To: T* Shared. O(1) Shared. O(1)
To: CountedPtrOrPtr<T> Shared. O(1) Shared. O(1) Shared. O(1), secure Shared. O(1), secure
Note
When choosing an Alias<T> parameter, the programmer should really consider using a CountedPtrOrPtr<T> member to store it, since it is the most secure-able variant. The only advantage of the two others storing methods (T& and T*) is that there is one less boolean test and indirection.
A conversion to a T* requires the use of the address operator (operator&) by the developer. If argument is Alias<T> a, and member name is b:
member type: T& T* CountedPtrOrPtr<T>
conversion: automatic: b(a) address: b(&a) automatic: b(a)
Advanced:
As one can see, if the programmer has chosen a CountedPtrOrPtr<T> for storing the alias, the user can choose between a secure long-term aliasing (with a CountedPtr<T> or CountedPtrOrPtr<T> argument) or non secure long-term aliasing (with a T& or T* argument).
struct BigFatA { ... };
struct B {
// versatile constructor. Accepts 4 different versions.
B( Alias<BigFatA> a ) : mySharedFat( a ) {}
void f() { mySharedFat->do(); }
CountedPtrOrPtr<BigFatA> mySharedFat;
};
BigFatA a1;
B b1( a1 ); // aliased (unsecure)
BigFatA adr = new BigFatA( "I am the one " )
B b2( adr ); // aliased
CountedPtr<BigFatA> counted_ptr = new BigFatA( "I am the other one" );
B b3( counted_ptr ); // secure aliased
delete counted_ptr; // b3 is still fine.
delete adr;

User passing an argument to a ConstAlias parameter

A parameter ( ConstAlias<T> cT ) accepts several types of arguments constructed from T. The true behavior of ConstAlias depends also on the way the programmer uses the object cT in the function/method. The object cT may be used to initialize 3 different types of object (often a data member): T&, T*, CowPtr<T>. The following table sums up the different conversions Argument toward member.

Argument const T& const T* CountedPtr<T> CountedPtrOrPtr<T> CountedConstPtrOrConstPtr<T>
To: const T& Shared. O(1) Shared. O(1)
To: const T* Shared. O(1) Shared. O(1)
To: CountedConstPtrOrConstPtr<T> Shared. O(1) Shared. O(1) Shared. O(1), secure Shared. O(1), secure Shared. O(1), secure
Note
When choosing a ConstAlias<T> parameter, the programmer should really consider using a CountedConstPtrOrConstPtr<T> member to store it, since it is the most secure-able variant. The only advantage of the two others storing methods (const T& and const T*) is that there is one less boolean test and indirection.
A conversion to a const T* requires the use of the address operator (operator&) by the developer. If argument is ConstAlias<T> a, and member name is b:
member type: const T& const T* CountedConstPtrOrConstPtr<T>
conversion: automatic: b(a) address: b(&a) automatic: b(a)
Advanced:
As one can see, if the programmer has chosen a CountedConstPtrOrConstPtr<T> for storing the const alias, the user can choose between a secure long-term const-aliasing (with a CountedPtr<T> or CountedPtrOrPtr<T> or CountedConstPtrOrConstPtr<T> argument) or non secure long-term const-aliasing (with a T& or T* argument).
struct BigFatA { ... };
struct B {
// versatile constructor. Accepts 5 different versions.
B( ConstAlias<BigFatA> a ) : myConstSharedFat( a ) {}
void f() const { myConstSharedFat->do(); } // const method
CountedConstPtrOrConstPtr<BigFatA> myConstSharedFat;
};
BigFatA a1;
B b1( a1 ); // const-aliased (unsecure)
BigFatA adr = new BigFatA( "I am the one " )
B b2( adr ); // aliased
CountedPtr<BigFatA> counted_ptr = new BigFatA( "I am the other one" );
B b3( counted_ptr ); // secure aliased
delete counted_ptr; // b3 is still fine.
delete adr;