1

A while ago, I found in a website some code examples of utility functions that are used when creating, destructing objects, or even when overloading some of their operators. More precisely, the following member functions are mainly used: init, copy, set, and destroy.

  • The init member function is used to initialize all the private members. It's mostly called inside the constructors, e.g. the default or parameter constructor.
  • The copy member function is used to do a deep copy of an object passed as a const reference. It is called inside the reference constructor and the overload of the operator =.
  • The set member function which mainly allocates memory for the private members that require it.
  • Finally, the destroy member function is used for releasing the allocated memory. It's called, for example, inside of the destructor.

I would like to have your opinion and know if this is a good programming practice? Which are the benefits or drawbacks? Any comments and suggestions are welcomed! Below, I'm illustrating how those member functions are defined for a CMatrix<T> class.

matrix.h

template < class T >
class CMatrix{

    CMatrix(){ this->initMatrix(); }

    CMatrix(int nRows, int nCols, int nChannels){
        this->initComplexMatrix();
        this->setComplexMatrix(nRows, nCols, nChannels);
    }

    CMatrix(const CMatrix<T> & refMatrix){
        this->initComplexMatrix();
        this->copyComplexMatrix(refMatrix);
    }

    CMatrix<T> & operator = (const CMatrix<T> & refMatrix){
        if(this!=&refMatrix){
            this->destroyComplexMatrix();
            this->initComplexMatrix();
            this->copyComplexMatrix(refMatrix);
        }
        return (*this);
    }

    T & CMatrix<T>::operator()(int, int, int);
    T CMatrix<T>::operator()(int, int, int) const;

    ......

    void initMatrix();
    void copyMatrix(const CMatrix<T> & );
    void setMatrix(int, int, int = 1);
    void destroyMatrix();

    ......

    ~CMatrix(){ this->destroyMatrix(); }

private:
    T *** m_pData;
    int m_nRows;
    int m_nCols;
    int m_nChannels;
};

matrix.cpp

#include <matrix.h>

template < class T >
inline T & CMatrix<T>::operator()(int mrow, int mcol, int mchannel){

    assert(mrow >= 0 && mrow < this->getRows());
    assert(mcol >= 0 && mcol < this->getCols());
    assert(mchannel >= 0 && mchannel < this->getChannels());

    return this->m_pData[mrow][mcol][mchannel];
}

template < class T >
void CMatrix<T>::initMatrix(){
    this->m_nRows   = 0;
    this->m_nCols   = 0;
    this->m_nChannels= 0;
    this->m_pData   = NULL;
}

template < class T >
void CMatrix<T>::copyMatrix(const CMatrix<T> & refMatrix){

    if(refMatrix.m_pData!=NULL){

        this->setMatrix(refMatrix.getRows(), refMatrix.getCols(), refMatrix.getChannels());

        for(register int dy=0; dy < this->getRows(); dy++){
            for(register int dx=0; dx < this->getCols(); dx++){
                for(register int ch=0; ch < this->getChannels(); ch++){ 
                    this->m_pData[(dy)][(dx)][(ch)] = refMatrix.m_pData[(dy)][(dx)][(ch)];
                }
            }
        }
    }
    else{
        this->m_pData = NULL;
    }
}

template < class T >
void CMatrix<T>::setMatrix(int nRows, int nCols, int nChannels){

    this->destroyMatrix();

    this->m_pData = NULL;
    this->m_pData = new T ** [nRows];

    for(register int dy=0; dy < nRows; dy++){
        this->m_pData[dy] = NULL;
        this->m_pData[dy] = new T * [nCols];
        for(register int dx=0; dx < nCols; dx++){
            this->m_pData[dy][dx] = NULL;
            this->m_pData[dy][dx] = new T[nChannels];
        }
    }

    this->setRows(mrows);
    this->setCols(mcols);
    this->setChannels(mchannels);
}

template < class T >
void CMatrix<T>::destroyMatrix(){

    if(this->m_pData!=NULL){

        for(register int dy=0; dy < this->getRows(); dy++){
            for(register int dx=0; dx < this->getCols(); dx++){
                delete [] this->m_pData[dy][dx];
            }
            delete [] this->m_pData[dy];
        }

        delete [] this->m_pData;
        this->m_pData = NULL;
    } 
}
Javier
  • 1,101
  • 4
  • 15
  • 22
  • from a top level look, seems pretty logical to me – Kenny Cason Feb 17 '11 at 12:59
  • 2
    What if some members are const? Then you need to use the initialization list. – Sam Miller Feb 17 '11 at 13:01
  • 2
    @Sam: Also if any members cannot be default-constructed. Also to create `const` instances of the parent object. – Ben Voigt Feb 17 '11 at 13:05
  • @Sam, you're right. @Ben, why do members couldn't be default-constructed? – Javier Feb 17 '11 at 13:06
  • 1
    Also, templated classes usually are [defined inline](http://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file). It looks like your example will produce linker errors for anything but a contrived example. – Sam Miller Feb 17 '11 at 13:08
  • @Sam Nice link, I was not aware of that – Kenny Cason Feb 17 '11 at 13:10
  • 1
    @Javier: Because the member's type has no constructor with zero arguments? – Ben Voigt Feb 17 '11 at 13:10
  • @Sam, that's not true. I just included part of my code. I actually do explicit instantiation. http://stackoverflow.com/questions/4199143/explicit-instantiation-of-parameterized-template-methods – Javier Feb 17 '11 at 13:10
  • Guys, any suggestions on how could i improve my code? would it be: (1) initialization lists in the constructors?, (2) try...catch blocks inside of setMatrix? codepad.org/4sweR3kU – Javier Feb 17 '11 at 17:43

2 Answers2

5

No, this is not recommended. The way you propose is not exception safe and is not compatible with const or subobjects that need non-default construction.

Instead use the ctor-initializer-list. Code reuse can be achieved through static helper functions called in the ctor-initializer-list or by moving logic into subobject constructors.

For memory allocation, use a subobject per resource. The memory management logic ends up in the subobject constructor and destructor. In many cases, you can use existing RAII classes from the library, such as std::vector, and not need to write any memory management code yourself.

Most operators can reuse the logic in the constructors, using the copy-and-swap idiom.

EDIT: Exception-safe construction might look something like this:

#include <vector>
template<typename T>
class matrix
{
    int m_nCols;
    std::vector<T*> m_rows;
    std::vector<T> m_cells;
    size_t makeIndex( int row, int col ) const { return row*m_nCols + col; }

public:    
    matrix( int nRows, int nCols )
        : m_nCols(nCols), m_rows(nRows), m_cells(nRows * nCols)
    {
        while (nRows--) m_rows[nRows] = &m_cells[nRows * nCols];
    }

    matrix( const matrix<T>& other )
        : m_nCols(other.m_nCols), m_rows(other.m_rows.size()), m_cells(other.m_cells)
    {
        int nRows = other.m_rows.size();
        while (nRows--) m_rows[nRows] = &m_cells[nRows * nCols];
    }

    void swap( matrix& other )
    {
        using std::swap;
        swap(m_nCols, other.m_nCols);
        swap(m_rows, other.m_rows);
        swap(m_cells, other.m_cells);
    }

    matrix& operator=( matrix other )
    {
        other.swap(*this);
        return *this;
    }

    const T& operator()( int row, int col ) const { return m_cells[makeIndex(row,col)]; }
    T& operator()( int row, int col ) { return m_cells[makeIndex(row,col)]; }
};

The std::vector destructor will take care of freeing the memory, and since the two allocations are separate objects, if m_cells fails to allocate its memory, the destructor for m_rows will run and nothing will leak.

Of course, std::vector is a little bit overkill here, a fixed-size array RAII class would be sufficient. But std::auto_ptr can't be used with arrays. I think C++0x is supposed to add a standard fixed-size RAII array class.

EDIT: Per request, a 3-D version:

#include <vector>
template<typename T>
class cube
{
    int m_nCols, m_nRows;
    std::vector<T> m_cells;
    size_t makeIndex( int row, int col, int channel ) const { return (channel*m_nRows + row)*m_nCols + col; }

public:    
    cube( int nRows, int nCols, int nChannels )
        : m_nCols(nCols), m_nRows(nRows), m_cells(nRows * nCols * nChannels)
    {
    }

    cube( const cube<T>& other )
        : m_nCols(other.m_nCols), m_nRows(other.m_nRows), m_cells(other.m_cells)
    {
    }

    void swap( cube& other )
    {
        using std::swap;
        swap(m_nCols, other.m_nCols);
        swap(m_nRows, other.m_nRows);
        swap(m_cells, other.m_cells);
    }

    cube& operator=( cube other )
    {
        other.swap(*this);
        return *this;
    }

    const T& operator()( int row, int col, int channel ) const { return m_cells[makeIndex(row,col,channel)]; }
    T& operator()( int row, int col, int channel ) { return m_cells[makeIndex(row,col,channel)]; }

    class channel_iterator
    {
        cube& const cube;
        int const row, col;
        int channel;
        friend class cube;
        channel_iterator( cube& all, int r, int c, int n ) : cube(all), row(r), col(c), channel(n) {}
    public:
        T& operator*() const { return cube(row, col, channel); }
        channel_iterator& operator++() { ++channel; return *this; }
        channel_iterator operator++(int) { return channel_iterator(cube, row, col, channel++); }
        bool operator!=(const channel_iterator& other) const { assert(&cube == &other.cube); return (row == other.row && col == other.col && channel == other.channel); }
    };

    int channel_count() const { return m_cells.size() / m_nRows / m_nChannels; }
    pair<channel_iterator, channel_iterator> range(int row, int col) { return make_pair(channel_iterator(*this, row, col, 0), channel_iterator(*this, row, col, channel_count())); }
};
Community
  • 1
  • 1
Ben Voigt
  • 260,885
  • 36
  • 380
  • 671
  • +1 for RAII classes, it seems the OP has not grasped that concept yet. – Sam Miller Feb 17 '11 at 13:23
  • @Ben, thanks for the explanations. Do you've any examples of `static helper functions called in the ctor-initializer-list`? And why is the code not `exception safe`? – Javier Feb 17 '11 at 13:43
  • Just a quick example of not being exception-safe: When the copy constructor calls `setMatrix` (via `copyMatrix`), if allocation fails on say `dx = 10`, what happens to all the data that's already been allocated? The object hasn't finished constructor, so the destructor doesn't run, and all that memory is leaked. Once you write exception-safe allocation code, you'll want to use it everywhere, so e.g. an `alloc(rows, cols)` helper function, which can then be used like: `CMatrix(int nRows, int nCols, int nChannels) : m_nRows(nRows), m_nCols(nCols), m_pData(alloc(nRows, nCols)) {}`. – Ben Voigt Feb 17 '11 at 13:53
  • But RAII would be better yet. – Ben Voigt Feb 17 '11 at 13:53
  • @Ben, thanks! Now, I get your point! You're right, it's not `exception-safe` :S But do you have an example on how an `exception-safe allocation code` would look like? How different would it be from `setMatrix`? What if I insert a `try-catch` block? I haven't worked with RAII classes, but will have a look. If you know any good tutorial/book on that, please let me know. – Javier Feb 17 '11 at 14:22
  • @Ben, thanks for the code sample! Now, things are becoming more clear! In my implementation, I overloaded the `operator()(int, int, int)` as sth. like: `return this->m_pData[mrow][mcol][mchannel];` to obtain the value a given position. How could I do something equivalent in your implementation? I edited my original post, where I added the implementation for the `operator()` overload. – Javier Feb 17 '11 at 15:13
  • @Javier: I fixed a typo in my example. My example is only two-dimensional, and it works just the way you'd expect (`m_rows[mrow][mcol]` returns the item at location `(mrow, mcol)`). You can also use `m_cells[mrow * m_nCols + mcol]`. If all access is done through `operator()()`, then the `m_rows` vector isn't needed. – Ben Voigt Feb 17 '11 at 16:00
  • @Ben, thanks! BTW, do you think that the following implementation avoids the drawbacks of my implementation? http://www.eld.leidenuniv.nl/~moene/Home/tips/matrix2d/ (A block per row and a column). – Javier Feb 17 '11 at 16:35
  • @Javier: My implementation is the same as #3 on that page. So they have the right idea. But their code isn't any more exception-safe than yours. – Ben Voigt Feb 17 '11 at 17:02
  • @Ben, thanks for the whole feedback! But, what it still not clear to me is how could I improve my existing code: (1) use initialization lists in the constructors?, (2) if i use `try...catch` blocks inside of setMatrix, would my code be then exception-safe? http://codepad.org/4sweR3kU – Javier Feb 17 '11 at 17:19
  • @Ben, BTW in your implementation the `m_rows(nRows)` in the initialization list of first constructor, would it be equivalent to: `m_rows=nRows`? `m_rows` is declared as a vector of T*, while `nRows` is an integer. I didn't get that part. – Javier Feb 17 '11 at 17:55
  • @Javier: The [`std::vector` constructor](http://www.cplusplus.com/reference/stl/vector/vector/) takes the number of elements. So it's roughly equivalent to `m_rows = new T*[nRows]`, except that `std::vector` will manage memory deallocation later with no extra effort on your part. – Ben Voigt Feb 17 '11 at 18:03
  • Also, yes your snippet on codepad has try/catch in the right places. But the cleanup when an exception is caught is not going to be pretty. – Ben Voigt Feb 17 '11 at 18:05
  • @Ben, great! Whenever you have time, I would be grateful if you could post a solution for a 3d matrix. I'm really learning new ways of programming through the snippets. – Javier Feb 17 '11 at 18:51
  • @Javier: If you remind me in about 6 hours, I'll be off work and can put together a larger example. – Ben Voigt Feb 17 '11 at 19:30
  • @Ben, thanks for the 3d matrix update! I think, the way you're computing the appropriate index in the `makeIndex` function is not correct, is it? – Javier Feb 18 '11 at 12:39
  • @Javier: It looks right to me, does it not give a unique index for every (x,y,z) triple? It's intended to give a layout of R1C1N1,R1C1N2,R1C1N3,R1C2N1,R1C2N2,...R1CyN1,..R1CyN3,R2C1N1,... – Ben Voigt Feb 18 '11 at 13:50
  • @Ben, oK, I see. I was having in mind: row1_col1_channel1 ...,row1_colN_channel1,... rowM_col1_channel1, ..., rowM_colN_channel1; ..., row1_col1_channelC ... row1_colN_channelC, rowM_col1_channelC...rowM_colN_channelC – Javier Feb 18 '11 at 14:14
  • @javier: Ok, that can be arranged. – Ben Voigt Feb 18 '11 at 14:17
  • @Ben: at MakeIndex sth. like: index = channels*(m_nRows*m_nCols) + (row*m_nCols+col); I guess the rest of the code remains the same, right (only MakeIndex changes, right)? – Javier Feb 18 '11 at 14:22
  • @Javier: I already changed it. And yeah, the only other thing that changed was now we have to keep track of the number of rows instead of the number of channels, but you might find it useful to keep track of all three dimensions anyway. – Ben Voigt Feb 18 '11 at 14:28
  • @Ben, I was wondering how could I retrieve data from the 'cube' object in an `ordered` way?. A first possibility, would be to have 3 nested `for` loops. One for each dimension, and inside I just need to call the `operator()`. What if I would like to use `iterators` to do sth. similar? so as to retrieve: R1_C1_N1,...,RM_CN_N1; R1_C1_N2,..., RM_CN_N2;...? – Javier May 05 '11 at 13:11
  • @Javier: If you mean iterating the entire matrix, that can be handled just by adding accessors which return `m_cells.begin()` and `m_cells.end()`. If you mean to iterate through a rectangular subset, you'll need to implement your own iterator type that stores the region to iterate and the current position, and implements `operator++` for any ordering you choose. – Ben Voigt May 05 '11 at 13:33
  • @Ben, thanks for the suggestion. I actually need both. I need to read a PPM image, i.e. triples of type (RGB, RGB, ...). I have 2 nested `for` loops. One for the rows, the other for the columns, and then I do sth. like: (*this)(dy, dx, 0)=R, (*this)(dy, dx, 1) = G, (*this)(dy, dx, 2) = B; and I was wondering how to profit of using `iterators` and without needing to use `nested loops`? Is it possible to do sth. like that? – Javier May 05 '11 at 14:41
  • @Javier: It is possible, but the for loops would be a lot cleaner. Indexing images by coordinate is very natural, I don't think you gain anything design-wise by using iterators, and the optimizer may or may not be able to make the iterator code as fast as the loops. You can make your syntax a little bit nicer though by making a named function that does the exact same thing as `operator()`, then you can write `at(dy, dx, 0) = R;` instead of `(*this)(dy, dx, 0) = R;` – Ben Voigt May 05 '11 at 15:28
  • @Ben, do you think it's a good idea to define the methods like `at(...)` or `make_index(...)` as being `inline`? I read that there's alway a trade-off, but that this makes usually sense for 1-line methods that are used several times. – Javier Jun 13 '11 at 19:15
  • @Javier: Yes, simple methods should usually be inline. Note that this is automatic for any function body which is inside the class declaration, it's only functions defined later that need to be specially marked with the `inline` keyword. (And today's compilers will inline small functions even without that hint.) – Ben Voigt Jun 13 '11 at 19:48
  • @Ben: as for the `cube` class, which would be the correct way to define a method like `vector & operator()( int row, int col)`? I would like to retrieve all the data at position `(row,col)` across the `channels` of a given `cube` object. – Javier Jul 05 '11 at 10:18
  • @Javier: You can't get a vector reference out of that. You can either return a vector, which would be a copy of the data, or my suggestion would be to return a `pair`, also called a range, which can be used to access the data in place. – Ben Voigt Jul 05 '11 at 13:16
  • @Ben, thanks a lot! Do you mind in giving an illustration for such a method for the `cube` class? What I'm trying to do is: (1) assign/retrieve a complete vector from a given position (x,y), i.e. the values across the depth, and (2) given a position (x,y) i would like to delete the corresponding vector, this means that also the size of the cube will change (instead of having MxNxD elements, it will have now (MxN-1)xD elements) – Javier Jul 09 '11 at 10:16
  • @Javier: You can't delete just a single (x,y) pair, you have to either delete all values sharing the same x, or all values sharing the same y. – Ben Voigt Jul 09 '11 at 17:23
  • @Javier: I threw together an iterator example, but I haven't tested it. – Ben Voigt Jul 09 '11 at 17:31
1

A better pattern for implementing assignment is the copy and swap idiom.

X& X::operator= (const X& rhv)
{
    X copy(rhv); //reuse copy constructor
    this->swap(copy); //reuse swap method - also useful for the user of the class!
    //previously held resources automatically released by the destructor of copy
} 

This performs the necessary steps in an order that keeps left-hand value unchanged if an exception occurs during the copying: doesn't release resources before new resources have been successfully obtained.

And the advantage over the methods you have is that the swap method is useful not only internally for implementing the class, but also for the user of the class. In case of the CMatrix class the implementation would be:

void CMatrix<T>::swap(CMatrix<T>& other)
{
    std::swap(m_pData, other.m_pData);
    std::swap(m_nRows, other.m_nRows);
    std::swap(m_nCols, other.m_nCols);
    std::swap(m_nChannels, other.m_nChannels);
}

Also, if suitable, it is a better idea to reuse existing RAII classes instead of managing memory manually in each and every class.

visitor
  • 7,738
  • 2
  • 24
  • 15
  • @visitor, thanks for the suggestion. in the case of the `operator =`, you're reusing the `copy constructor`. For my `CMatrix` class how does the `copy constructor` would be affected, or its implementation remains the same? – Javier Feb 17 '11 at 14:12
  • @Javier: The constructors might reuse a function to allocate the array. Beyond that it seems that your implementation just does unnecessary work: e.g if the allocation function also initializes arrays to zeros, then the copy constructor would first set all the values to zeros only to copy over the actual values. – visitor Feb 17 '11 at 14:55
  • @visitor: You might want to read GMan's epic discussion on copy-and-swap I linked in my answer, it suggests a number of ways to improve your solution. – Ben Voigt Feb 17 '11 at 15:00
  • @Ben: When first presenting the idiom to someone, I don't think it is that important to emphasize the subtle optimization possibilities. – UncleBens Feb 17 '11 at 16:21
  • @UncleBens: Unfortunately sample code is likely to get pasted and used as-is. While one doesn't have to explain every detail of WHY an argument is made pass-by-value instead of using a local variable to make the copy, or WHY `std::swap` is found by a using-declaration instead of explicit name qualification, it's still good to follow best practices in example code. Especially when they make the example shorter, as they do here. – Ben Voigt Feb 17 '11 at 16:26
  • @Ben: Actually I don't see why ADL should be needed to swap a (triple-)pointer and a few ints. – UncleBens Feb 17 '11 at 16:51
  • @UncleBens: It's not -- for these particular types. But again, it's very likely that code examples are going to get used as-is, and almost as likely that they'll be reused in other situations with other data types where ADL might be very important. – Ben Voigt Feb 17 '11 at 16:59