0

I have a function in which the nodes of a binary 'tree' are populated with values recursively computed based on the input vector, which represents the values on the leaves. An old C++ implementation of the function is as follows

using namespace std;

double f(const size_t h, vector<double>& input) {
    double** x;
    x = new double*[h+1];
    x[0] = input.data();

    for (size_t l = 1; l <= h; l++)
            x[l] = new double[1<<(h-l)];
    // do the computations on the tree where x[l][n] is the value 
    // on the node n at level l.


    result = x[l][0];

    for (size_t l = 1; l <= h; l++)
            delete[] x[l];

    delete[] x;

    return result;
}

Now I'm trying to write a 'modern' implementation of the code using smart pointers in C++11/C++14. I attempted to define x using std::unique_ptr specialization for arrays so that I do not have to change the 'computation' procedure. The obvious problem with such an approach is that the contents of `input' will be deleted at the end of the function (because the unique pointer that takes the ownership of the data will be destroyed at the end of the function).

One simple (and perhaps safe) solution would be to allocate the memory for the whole tree (including the leaves) in x and copy the values of the leaves from input to x[0] in the beginning of the function (in this case I can even used nested std::vectors instead of std::unique_ptrs specialized for arrays as the type of x). But I prefer to avoid the cost of copying.

Alternatively one can change the computational procedures to read the values of the leaves directly from input not from x which requires changing too many small pieces of the code.

Is there any other way to do this?

MikeL
  • 2,369
  • 2
  • 24
  • 38
  • A niggle, but input won't be deleted when the unique_ptr x destroys the array it owns. Deletion isn't recursive in C++. x[0] = input.data() is talking to the array the smart pointer owns, not the pointer itself. – Art Yerkes Apr 06 '17 at 14:35
  • You could make a `std::vector>` for your "temporary" arrays and a `std::vector*>` where you point to your input array and your temporary arrays. – François Andrieux Apr 06 '17 at 14:37
  • 1
    note: Any good reason for the rewrite? – Karoly Horvath Apr 06 '17 at 14:41
  • @KarolyHorvath mainly because I think the way it is written may have memory leak problems (the memory deletion part at the end of the function is indeed repeated several times in the middle depending on when the computations end, ...). Also the fact that the input is not taken as `const std::vector&` bothers me. – MikeL Apr 06 '17 at 14:46
  • @KarolyHorvath Presumably, the allocated arrays would be mutable, while your input is const. If this is correct, you would need to declare some type of array to pointers where only the first element is `const`. This seems like a challenging type to represent, to say the least. Perhaps you would be better off keeping your input separate from your tree? – François Andrieux Apr 06 '17 at 14:51

3 Answers3

1

C++11/14 didn't really introduce anything that wasn't already achievable prior using the modern std::vector for managing the memory of dynamic arrays.

The obvious problem with [std::unique_ptr] is that the contents of `input' will be deleted at the end of the function

Indeed. You may not "steal" the buffer of the input vector (except into another vector, by swapping or moving). This would lead to undefined behaviour.

Alternatively one can change the computational procedures to read the values of the leaves directly from input not from x which requires changing too many small pieces of the code.

This alternative makes a lot of sense. It is unclear why the input vector must be pointed by x[0]. The loops start from 1, so it appears to not be used by them. If it is only ever referenced directly, then it would make much more sense to use the input argument itself. With the shown code, I expect that this would simplify your function greatly.

Also the fact that the input is not taken as const std::vector& bothers me.

This is another reason to not point to the input vector from the modifiable x[0]. The limitation can however be worked around using const_cast. This is the kind of situation what const_cast is for.

Let us assume henceforth that it makes sense for the input to be part of the local array of arrays.

One simple (and perhaps safe) solution would be to allocate the memory for the whole tree (including the leaves) in x ... I can even used nested std::vectors ... But I prefer to avoid the cost of copying.

You don't necessarily need to copy if you use a vector of vectors. You can swap or move the input vector into x[0]. Once the processing is complete, you can restore the input if so desired by swapping or moving back. None of this is necessary if you keep the input separate as suggested.


I suggest another approach. The following suggestion is primarily a performance optimization, since it reduces the number of allocations to 2. As a bonus, it just so happens to also easily fit with your desire to point to input vector from the local array of arrays. The idea is to allocate all of the tree in one flat vector, and allocate another vector for bare pointers into the content vector.

Here is an example that uses the input vector as x[0], but it is easy to change if you choose to use input directly.

double f(const size_t h, const std::vector<double>& input) {
    std::vector<double*> x(h + 1);
    x[0] = const_cast<double*>(input.data()); // take extra care not to modify x[0]

    // (1 << 0) + (1 << 1) + ... + (1 << (h-1)) == (1 << h) - 1
    std::vector<double> tree((1 << h) - 1);
    for (std::size_t index = 0, l = 1; l <= h; l++) {
        x[l] = &tree[index];
        index += (1 << (h - l));
    }

    // do the computations on the tree where x[l][n] is the value 
    // on the node n at level l.

    return x[l][0];
}
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • I think the loop starting by 1 in the OP's example just shows how the data structure is initially filled. I assume the code which is not shown and which performs the actual algorithmitic processing does use `[0]` as well; otherwise the question would be pretty meaningless. – Christian Hackl Apr 06 '17 at 16:00
  • @ChristianHackl possibly. Perhaps not. It's not possible to tell. – eerorika Apr 06 '17 at 16:01
  • @ChristianHackl Maybe you have a point. I changed the example to use `[0]` as input. – eerorika Apr 06 '17 at 17:33
0

This certainly looks like a job for a std::vector<std::vector<double>>, not std::unique_ptr, but with the additional complexity that you conceptually want the vector to own only a part of its contents, while the first element is a non-owned reference to the input vector (and not a copy).

That's not directly possible, but you can add an additional layer of indirection to achieve the desired effect. If I understand your problem correctly, you want to behave x such that it supports an operator[] where an argument of 0 refers to input, whereas arguments > 0 refer to data owned by x itself.

I'd write a simple container implemented in terms of std::vector for that. Here is a toy example; I've called the container SpecialVector:

#include <vector>

double f(const std::size_t h, std::vector<double>& input) {

    struct SpecialVector {
        SpecialVector(std::vector<double>& input) :
            owned(),
            input(input)
        {}

        std::vector<std::vector<double>> owned;
        std::vector<double>& input;

        std::vector<double>& operator[](int index) {
            if (index == 0) {
                return input;
            } else {
                return owned[index - 1];
            }
        }

        void add(int size) {
            owned.emplace_back(size);
        }
    };

    SpecialVector x(input);

    for (std::size_t l = 1; l <= h; l++)
            x.add(1<<(h-l));
    // do the computations on the tree where x[l][n] is the value 
    // on the node n at level l.

    auto result = x[1][0];

    return result;
}

int main() {
    std::vector<double> input { 1.0, 2.0, 3.0 };
    f(10, input);
}

This approach allows the rest of the legacy code to continue to use [] exactly as it did before.

Christian Hackl
  • 27,051
  • 3
  • 32
  • 62
  • Elegant! It looks like exactly what I was searching for! I think we can even make `input`s `const` in this implementation. – MikeL Apr 06 '17 at 15:19
  • @PBM: You're welcome :) It doesn't solve the `const` problem, though. I doubt that can be really solved elegantly without a major rewrite. Perhaps this is one the rare situations where `const_cast` would be a valid pragmatic and temporary solution. – Christian Hackl Apr 06 '17 at 15:24
  • The conditional branch in operator[] is expensive. Depending on code flow and data access pattern, it can potentially annihilate any benefit from branch-prediction. – Fabio Apr 06 '17 at 15:59
  • @Fabio: That's quite speculative, as we don't know the size of the data and how often the function is called. Chances are it simply doesn't matter. I'd say if the difference is *actually* noticeable with real data, then that's one more reason to rewrite the entire algorithm, taking into account this non-functional requirement. – Christian Hackl Apr 06 '17 at 16:07
  • Christian, suppose you access rows in order i,0,j,0,k,0... branch prediction is completely annihilated as no learning is possible. It is true that we do not know if operator[] is a performance critical function or not. If it is not, inefficient code does not harm. Still writing inefficient code is imho not ideal. – Fabio Apr 06 '17 at 16:14
  • @Fabio: I don't disagree with your general observation about branch prediction. I disagree with the suggestion that it's relevant in this specific case. Most code is not performance-critical. So I'd say that this example is not inefficient until proven otherwise, but replacing it with a less robust solution is premature optimisation. – Christian Hackl Apr 06 '17 at 16:25
0

Write a class Row, which contains a flag for ownership controlling destruction behavior and implement operator[], then create a vector of row.

As noted above, you have issues if input is constant, as you cannot explicitly enforce it at compiler level, and you have to be careful not to write where you cannot, but this is not worse then what you have now.

I have not tried to compile it, but your new Row class could look a bit like this.

class Row
{
   double *p;
   bool f;
public:
   Row() :p(0), f(false) {}
   void init(size_t n) { p = new double[n]; f=true; }
   void init(double *d) { p=d;, f=false;}
   double operator[](size_t i) { return p[i]; }
   ~Row() { if (flag) delete[] p; }
};
Fabio
  • 2,105
  • 16
  • 26