2

I would like to further exhaust this topic.

Assume that I have something like:

class MyClass
{
public: 

MyClass(int N)
{ 
    data_ptr = new float[N];
};

float* dat_ptr;    

// ... clever operator definition here ...
};

So I would like to be able to simply write:

MyClass a(4);
MyClass b(4);
MyClass c(4);

// modify b.data_ptr and c.data_ptr ....
// Use "clever operator"
a = b + c;

Where the operator would do a.data_ptr[i] = b.data_ptr[i] + c.data_ptr[i] for i=0:(N-1) ...

Hence no extra copies of the data are created and we are neatly using the preallocated buffers.

Is this possible? If so, please provide me with som insights as to how it would be done.

Thanks!

utnapistim
  • 26,809
  • 3
  • 46
  • 82
Jimmy Pettersson
  • 465
  • 4
  • 13

3 Answers3

1

It is not possible; Before a is assigned to, a temporary object will be created as a result of calling operator + (b, c); This operator should return the created instance, that should then be assigned to a; the created instance is always created by b + c.

What is possible though is to define += as a member operator and say:

b += c;

This would modify the value of b without creating extra copies.

Edit: I have reconsidered :)

You definitely can do it, by abstracting operations as lazy evaluation objects.

Here is an example:

class MyClass; // fwd. declaration of your class

struct LazySum
{
    LazySum(const MyClass& a, const MyClass& b)
    : x(a), y(b) {}

    float operator[](int i) { return x[i] + y[i]; }

    const MyClass& x;
    const MyClass& y;
};

class MyClass
{
public: 
    MyClass(int N)
    { 
        data_ptr = new float[n = N];
    };

    int n;           // this shouldn't be public
    float* dat_ptr;  // nor this, but I went with your code

    // ... clever operator definition here ...
    MyClass& operator=(const LazySum& terms)
    {
        // ignore case when n != x.n or n != y.n
        // because not the point of the example
        // (and I'm lazy)

        // sum evaluation occurs here
        // with no new allocations
        for(int i = 0; i < n; ++i)
            data_ptr[i] = terms[i]; 
        return *this;
    }
};

LazySum operator=(const MyClass& x, const MyClass& y)
{
    return LazySum(x, y); // LazySum is a couple of references in size
}

void client_code_using_clever_op()
{
    MyClass a(4);
    MyClass b(4);
    MyClass c(4);

    // modify b.data_ptr and c.data_ptr ....
    // Use "clever operator"
    a = b + c; // actual sum performed when operator = is executed
}

The idea is to store the terms, and perform late evaluation on the terms.

Points of improvement:

  • inject a functor in the construction of LazySum to make it become LazyOp (the functor would decide what the op is); Implement other binary operators on MyClass in terms of it.

  • use RAII in MyClass.

  • when you need to implement lazy evaluation operators on another type (e.g. some MyOtherClass) consider implementing LazyOp as a template on the terms and functor type.

  • this does not support more complex expressions without some extra work:

    MyClass a(4), b(4), c(4), d(4);

    d = (a + b) + c; // error

    This example will not work because it would require an operator+(const LazySum&, const MyClass&);;

utnapistim
  • 26,809
  • 3
  • 46
  • 82
  • This has been my suspiscion. Thank you. I can however foresee potential workarounds using an external buffer manager (more tedious). – Jimmy Pettersson May 29 '13 at 09:42
  • Not necessarily true in C++11. I C++03, however, I agree that there is no way. – AlefSin May 29 '13 at 10:21
  • @AlefSin, in C++11, the extra copy is avoided only if you declare `a` in the line you assign to it (not the case with the example given by op). Otherwise an extra copy will be created in C++11 as well. – utnapistim May 29 '13 at 10:46
  • @JimmyPettersson, I have changed my answer: it came to me that you can use lazy evaluation on const references to avoid unnecessary allocations. – utnapistim Jun 01 '13 at 00:27
  • @AlefSin, I have found a way :) - see my answer above. – utnapistim Jun 01 '13 at 00:27
  • @utnapistim Seems to be an interesting solution. It seems that you are basically trying to parse the expression on the right side on you own rather than leaving it to the compiler. It will get more and more complex as more types of expressions are considered. I guess at the end, it will lead to something like expression templates used by Eigen and other libraries. – AlefSin Jun 01 '13 at 08:32
1

You can, if you use move semantics from C++11.

class MyClass
{
public: 

    MyClass(int N)
    { 
        data_ptr = new float[N];
        n = N;
    }

    MyClass(MyClass && rhs)
    {
        data_ptr = rhs.data_ptr;
        n = rhs.n;

        rhs.data_ptr = nullptr;
    }

    // dtor, copy-ctor etc.

    int n;
    float * dat_ptr;    
};

MyClass operator + (const MyClass & left, const MyClass & right)
{
    MyClass result(left.n);

    // Implement addition
}

// Note: no error-checking

This way a temporary object will be created, but the internal data will not be unnecessarily copied.

Read more about the move semantics.

Community
  • 1
  • 1
Spook
  • 25,318
  • 18
  • 90
  • 167
  • True. It is possible to do so. Also notice that for this example to behave correctly it also has to have a destructor and a copy constructor. I suggest further reading about how to implement a class that manages its resources (in this case, data_ptr)) [here](http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom) – AlefSin May 29 '13 at 10:20
  • @AlefSin You're right - I omitted these on purpose, to show only crucial parts of the code. I'll leave appropriate notice, though. Edited the post. – Spook May 29 '13 at 10:25
0

As Spook explained, yes it is possible. Just for fun I wrote a full example that you can compile and run. If a copy was to be created, you would get a message in the output. I tried this example in Visual Studio 2012 and runs fine.

class MyClass
{
private:
    float *data_ptr;
    std::size_t size;

public:
    MyClass(std::size_t N = 0) :
        size(N),
        data_ptr(N ? new float[N]() : nullptr)
    {}

    MyClass(const MyClass& other) :
        size(other.size),
        data_ptr(other.size ? new float[other.size]() : nullptr)
    {
        std::copy(other.data_ptr, other.data_ptr + size, data_ptr);
        std::cout << "Copy!" << std::endl;
    }

    MyClass(MyClass&& other) 
    {
        size = 0;
        data_ptr = nullptr;

        swap(*this, other);
    }

    ~MyClass()
    {
        delete[] data_ptr;
    }

    MyClass& operator=(MyClass other)
    {
        swap(*this, other);

        return *this;
    }

    friend MyClass operator+(MyClass& first, MyClass& second)
    {
        MyClass result(std::min(first.size, second.size));

        for (std::size_t i=0; i < result.size; i++) {
            result.data_ptr[i] = first.data_ptr[i] + second.data_ptr[i];
        }

        return result;
    }

    friend void swap(MyClass& first, MyClass& second)
    {
        std::swap(first.size, second.size);
        std::swap(first.data_ptr, second.data_ptr);
    }
};


int _tmain(int argc, _TCHAR* argv[])
{
    MyClass a(5);
    MyClass b(5);
    MyClass c(5);

    a = b + c; //this should not produce an extra copy

    return 0;
}
AlefSin
  • 1,086
  • 9
  • 20