0

I have a low-level embedded application, where I have some relatively large const, global, static arrays (lookup tables and such). The compiler (or linker) stores them in Flash memory rather than in RAM, since they are const.

Now, I have a class that needs to be initialized with one such array. It will use the data from that array throughout the lifetime of the class object.

My question is: how can I safely pass a pointer to this global, static array to the object, while preventing mistakenly passing an array with a short lifetime rather than a static one?

For example, consider the naive implementation that doesn't protect from incorrect initialization:

class Interpolator
{
public:
    Interpolator(const float table[], int size);
    float interpolate(float x);  // uses 'table' data member
private:
    const float* table;
    int size;
};


Interpolator::Interpolator(const float table[], int size) :
        table(table), size(size)
{
}


const float table1[] = {1.0, 2.0, 42.0 /* a few thousand more */ };

void main()
{
    Interpolator interpolator1(table1, sizeof(table1) / sizeof(float));
    float x = interpolator1.interpolate(17.0);  // OK

    float* table2 = new float[1024];
    // ... calculate and fill in values in table2 ...
    Interpolator interpolator2(table2, 1024);  // how to prevent this usage?
    delete[] table2;  // incorrectly assume the object created a copy for itself and the delete is safe...
    float y = interpolator2.interpolate(17.0);  // ERROR, undefined behavior
}

How do I prevent the second instantiation in the example? perhaps through constexpr somehow, or some clever usage of templates...?

Notes:

  • I realize that the problem here is that my class doesn't conform to RAII. However, under the constraints explained above (use a large static array from Flash memory), I don't see how I can make it conform to RAII.

  • Copying the data from the static array to a local data member in the object is out of the question - a single array may literally be larger than my whole RAM, which is only tens of kB in size.

  • I will have multiple instances of the class, multiple static data tables, and several instances of the class may be initialized with the same static data table.

Any idea for a design pattern that enforces safety here?

thanks!

1
  • my suggestion is simply don't care about this. If one can carefully manage the lifetime of interpolator2 and table2, why prevent it? (If one cannot, then there may be much more problem here and there, like invalid iterators) Commented Oct 11, 2018 at 3:39

1 Answer 1

4

The address of a variable is a constant expression. This means we can use the address of the table as a template argument.

In this way we can build a specific template class for each interpolation table that exists, and no others.

This removes the possibility of creating an interpolator which points to a transient table.

It also has the advantage of requiring less storage since it does not need to maintain pointers to the data.

example:

#include <cstddef>


template<const float* const Table, std::size_t Size>
struct InterpolatorImpl
{
public:
    float interpolate(float x)
    {
        // use Table and Size here as constant expressions
        // or write in terms of begin() and end()

        return 0;
    }

    constexpr std::size_t size() const
    {
        return Size;
    }

    constexpr const float* begin() const
    {
        return Table;
    }

    constexpr const float* end() const
    {
        return begin() + size();
    }
};

const float table1[] = {1.0, 2.0, 42.0 /* a few thousand more */ };
using Interpolator1 = InterpolatorImpl<table1, sizeof(table1) / sizeof(float)>;

const float table2[] = {1.0, 3.0, 5.0 /* a few thousand more */ };
using Interpolator2 = InterpolatorImpl<table2, sizeof(table2) / sizeof(float)>;


int main()
{
    Interpolator1 interpolator1;
    float x = interpolator1.interpolate(17.0);  // OK

    float y = Interpolator2().interpolate(21);    
}

But what if there were cases where we wanted to conditionally interpolate against one or another table?

In this case we could make the InterpolatorImpl polymorphic, deriving from a common base. We could then provide the common base with a means of performing interpolation based on table details acquired through a private virtual function.

#include <cstddef>


struct Interpolator
{
    float interpolate(float x) const
    {
        return interpolate(getDetails(), x);
    }

protected:

    struct Details
    {
        const float* first;
        std::size_t length;
    };

private:
    virtual Details getDetails() const = 0;

    static float interpolate(Details details, float x)
    {
        // do interpolation here
        auto begin = details.first;
        auto size = details.length;


        // ...

        return 0;
    }
};

template<const float* const Table, std::size_t Size>
struct InterpolatorImpl : Interpolator
{
public:
    constexpr std::size_t size() const
    {
        return Size;
    }

    constexpr const float* begin() const
    {
        return Table;
    }

    constexpr const float* end() const
    {
        return begin() + size();
    }

private:
    virtual Details getDetails() const override
    {
        return { Table, Size };
    }

    friend auto poly(InterpolatorImpl const& i) -> Interpolator const&
    {
        return i;
    }
};

const float table1[] = {1.0, 2.0, 42.0 /* a few thousand more */ };
using Interpolator1 = InterpolatorImpl<table1, sizeof(table1) / sizeof(float)>;

const float table2[] = {1.0, 3.0, 5.0 /* a few thousand more */ };
using Interpolator2 = InterpolatorImpl<table2, sizeof(table2) / sizeof(float)>;

float doInterpolation(Interpolator const& interp, float x)
{
    return interp.interpolate(x);
}

bool choice();

int main()
{
    Interpolator1 interpolator1;
    Interpolator2 interpolator2;

    float x = doInterpolation(choice() ? poly(interpolator1) : poly(interpolator2) , 17.0);  // OK

}

But what if my compiler is a little old and does not treat the address of a variable as a constant expression?

Then we need a little hand-rolling for each interpolator:

#include <cstddef>
#include <type_traits>


struct Interpolator
{
    float interpolate(float x) const
    {
        return interpolate(getDetails(), x);
    }

protected:

    struct Details
    {
        const float* first;
        std::size_t length;
    };

private:
    virtual Details getDetails() const = 0;

    static float interpolate(Details details, float x)
    {
        // do interpolation here
        auto begin = details.first;
        auto size = details.length;


        // ...

        return 0;
    }

    friend Interpolator const& poly(Interpolator const& self) { return self; }
};


const float table1[] = {1.0, 2.0, 42.0 /* a few thousand more */ };
struct Interpolator1 : Interpolator
{
    virtual Details getDetails() const override
    {
        return {
            table1,
            std::extent<decltype(table1)>::value
        };
    }
};

const float table2[] = {1.0, 3.0, 5.0 /* a few thousand more */ };
struct Interpolator2 : Interpolator
{
    virtual Details getDetails() const override
    {
        return {
            table2,
            std::extent<decltype(table2)>::value
        };
    }
};

float doInterpolation(Interpolator const& interp, float x)
{
    return interp.interpolate(x);
}

bool choice();

int main()
{
    Interpolator1 interpolator1;
    Interpolator2 interpolator2;

    float x = doInterpolation(choice() ? poly(interpolator1) : poly(interpolator2) , 17.0);  // OK

}

https://godbolt.org/z/6m2BM8

Sign up to request clarification or add additional context in comments.

9 Comments

Note a (non-function) pointer is a constant expression only if it points at an object with static storage duration or is a null pointer, so that would rule out the concern about incorrectly specializing InterpolatorImpl and using it outside the array's lifetime. A static_assert could also rule out the null pointer case, if really concerned about that.
Interesting Solution! I should note that it only compiles if the tables are declared as constexpr, rather than const.
@Tinkerer no, it'll work if the tables are not constexpr. The address of a statically allocated array is always a constant expression.
@Tinkerer gcc accepts it without constexpr since v7.2, clang accepts it. So maybe an old gcc bug that was fixed. godbolt.org/z/g34XMy
@Tinkerer updated answer with 3rd option which will work on your compiler.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.