Умные указатели и попытка избавиться от лишнего копирования
От: Andy the Toad Россия  
Дата: 29.03.05 00:35
Оценка:
Господа гуру от C++, помогите пожалуйста копнуть глубже в казалось бы тривиальную проблему — не сочтите за труд дочитать до конца. Есть желание реализовать некий класс, занимающий в памяти место, большее чем sizeof(int), однако у которого перегружены операторы, долженствующие возвращать сам экземпляр класса, а не указатель или ссылку на него. Пусть это будет TMatrix:
class TMatrix
{
    ...
public:
    friend TMatrix operator * (TMatrix &m1, TMatrix &m2);
    ...
};

Автоматически приходим к идее некоего "умного указателя", или точнее "ведущего указателя", когда сам массив данных выделяется динамически, а класс-обертка TMatrix содержит в себе указатель на этот массив. Тогда для реализации подержки операций вроде:
TMatrix m,m1,m2;

    m = m1 * m2;

нужно оформить наш класс например так:
struct TMatrixData{
    double a[4][4];
};
//--------------
class TMatrix
{
private:
    TMatrixData *Data;
public:
    TMatrix(){
        Data = new TMatrixData;
    };
    TMatrix(TMatrix &m){
        Data = new TMatrixData;
        memcpy(Data,m.Data,sizeof(*Data));
    }
    ~TMatrix(){
        delete Data;
    }
    TMatrix &operator = (TMatrix &m){
        memcpy(Data,m.Data,sizeof(*Data));
        return *this;
    }
    friend TMatrix operator * (const TMatrix &m1, const TMatrix &m2);
    ...
};
//--------------
inline TMatrix operator * (TMatrix &m1, TMatrix &m2)
{
TMatrix m; //(1)

    Data = m1->Data * m2->Data; //упрощенный вид реализации умножения

    return m; //(2)
}
//--------------

В точке (2) компилятор (как MSVC++ 6.0, так и C++ Builder 5) разворачивает код таким образом:
TMatrix rm = m;
m->~TMatrix();
return rm;

т.е. сначала вызывает copy-конструктор для некоего временного объекта, удаляет наш экземпляр m, созданный в точке (1) и возвращает из функции этот временный объект.
Зачем мне этот лишний вызов memcpy? Почему нельзя, чтобы код вида:
m = m1 * m2 * m3;

разворачивался так:
TempData1 = new TMatrixData;      //Data некоего временного объекта TMatrix
TempData1 = m1->Data * m2->Data;  //упрощенный вид реализации умножения
TempData2 = new TMatrixData;      //Data другого временного объекта TMatrix
TempData2 = TempData1 * m3->Data; //упрощенный вид реализации умножения
m->Data   = TempData2;
delete TempData1;
//TempData2 не удаляется, т.к. каким-то образом сигнализируется, что к нему был приравнен m->Data.

Т.е. хотелось бы избавиться от двух вызовов memcpy — одного явного, в операторе =, а другого неявного, в точке (2)
Если еще не надоело читать, то посмотрите мой подход к этой проблеме:
struct TMatrixData{
    double a[4][4];
    bool IsTemp;
};
//--------------
class TMatrix
{
private:
    TMatrixData *Data;
    TMatrix(TMatrixData *d){
        Data = d;
    };
public:
    TMatrix(){
        Data = new TMatrixData;
        Data->IsTemp = false;
    };
    TMatrix(TMatrix &m){
        if(m.Data->IsTemp){
            Data = m.Data;
            m.Data = NULL; //(1)
        }
        else{
            Data = new TMatrixData;
            memcpy(Data->a,m.Data->a,sizeof(*Data->a));
        }
        Data->IsTemp = false;
    }
    ~TMatrix(){
        if(Data) delete Data;
    }
    TMatrix &operator = (TMatrix &m){
        if(m.Data->IsTemp){
            if(Data) delete Data;
            Data = m.Data;
            m.Data = NULL; //(2)
            Data->IsTemp = false;
        }
        else{
            if(!Data){
                Data = new TMatrixData;
                Data->IsTemp = false;
            }
            memcpy(Data->a,m.Data->a,sizeof(*Data->a));
        }
        return *this;
    }
    friend TMatrix operator * (const TMatrix &m1, const TMatrix &m2);
    ...
};
//--------------
inline TMatrix operator * (TMatrix &m1, TMatrix &m2)
{
TMatrixData *d;

    d = new TMatrixData;
    d->IsTemp = true;

    d = m1->Data * m2->Data; //упрощенный вид реализации умножения

    if(m1.Data->IsTemp){ //(3)
        delete m1.Data;
        m1.Data = NULL;
    }
    if(m2.Data->IsTemp){ //(4)
        delete m2.Data;
        m2.Data = NULL;
    }

    return TMatrix(d);
}
//--------------

Т.е. во время умножения создается временный объект, который затем уничтожается в последующем вызове оператора * или переприсваивается в операторе =.
Есть несколько неудобств:
1) В точках (3) и (4) приходится явно отслеживать удаление m1.Data и m2.Data. Если набор операторов работы с объектом большой, то даже при оформлении этого момента в какую-либо функцию будет зря загромождаться код. Но это не так важно, тем более у меня сейчас возникла одна мысль — не зря ли я это делаю, но оставим это, ибо есть неудобство 2 и 3:
2) В точках (1) и (2) приходится модифицировать аргумент функции, поэтому этот аргумент нельзя объявить как const, а это плохо, потому что C++ Builder, будь он неладен, не понимает таких copy-конструкторов и операторов присваивания — в выражениях вида m = m1 * m2 он не понимает, что TMatrix нужно преобразовать к TMatrix&
3) Если мы где-нибудь в середине кода запишем TMatrix m = m1 * m2, то код развернется во всего один вызов оператора *, а объект m будет иметь Data->IsTemp == true, т.е. мы поимеем ссылку на временный объект, который не задумывался быть видимым для программиста. При первом же использовании его в операторах он примет значение NULL — понятно, чем это чревато.

Господа гуру — те, кто сумел дочитать все это до конца, подскажите, есть ли более изящное решение этой проблемы, свободное от указанных недостатков? Что здесь можно изменить или подправить или вообще переписать? Хотелось бы иметь хорошую общую идею, потому что классов, ждущих реализации по подобному принципу у меня уже как минимум четыре наберется.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.