3

I am hoping that it is possible to write a template class that will be inherited for several type-specific sub-classes. I want the inherited methods and operators to return the type of the sub-class rather than the parent template type. This is in hopes of saving lots of development and maintenance effort if I only have to modify one base class.

Here is an example of what I have already:

template<typename T> struct TMonoPixel
{
    T value;

    TMonoPixel(T v) { value = v; }

    // the template has some pure virtual functions here...

    TMonoPixel operator+ (const TMonoPixel& other)
    { return TMonoPixel(value + other.value); }
}

struct Mono8Pixel : TMonoPixel<uint8_t>
{
    using TMonoPixel::TMonoPixel;    // I want to inherit the constructor
    // each pixel type implements the virtual functions in the template
}

As you can see the Mono8Pixel struct inherits the + operator which accepts TMonoPixel, but using this operator returns TMonoPixel<uint8_t> rather than Mono8Pixel because it is defined in the base class.

I am planning to use these structs for iterating over pixels in an image:

Image* img; // img has an unsigned char* pointer to its pixel data
for (int row=0; row<img->height; row++) {
    for (int col=0; col<img->width; col++) {
        int i = (row*img->width + col);
        Mono8Pixel* pixel = reinterpret_cast<Mono8Pixel*>(img->dataPtr + sizeof(unsigned char)*i);
        // modify the pixel ...
    }
}

Is there any way to change just the template class to ensure that Mono8Pixel(2) + Mono8Pixel(2) is returning a Mono8Pixel?

Note that whatever the solution is, these structs must maintain standard layout because of how I wish to use them.

Romen
  • 1,617
  • 10
  • 26

1 Answers1

1

What you want can be done using the curiously recurring template pattern (CRTP). The basic idea is this:

template<class Pixel> struct TMonoPixel {
    ...

    // not virtual
    std::string GetSomeProperty() const {
        return static_cast<const Pixel&>(*this).GetSomeProperty();
    }

    Pixel operator+(const TMonoPixel& other) const {
        return Pixel(value + other.value);
    }
};

struct Mono8Pixel : TMonoPixel<Mono8Pixel> {
    using TMonoPixel::TMonoPixel;

    std::string GetSomeProperty() const {
        return "My name is Mono8Pixel";
    }
};

Thanks to implicit derived-to-base conversion now you can use it like this:

template<class T>
void foo(const TMonoPixel<T>& number) {
    std::cout << number.GetSomeProperty();    
}

Mono8Pixel i;
foo(i);

Note that inside TMonoPixel, Pixel is an incomplete type, so you have some limitations on how it can be used. For example, you can't do this:

template<class Pixel> struct TMonoPixel {
    Pixel::Type operator+(const TMonoPixel& other);
};

struct Mono8Pixel : TMonoPixel<Mono8Pixel> {
    using Type = std::uint8_t;
};

Type traits is a useful technique to overcome such limitations:

struct Mono8Pixel;

template<class Pixel> struct ValueType;

template<> struct ValueType<Mono8Pixel> {
    using Type = std::uint8_t;
};

template<class Pixel> struct TMonoPixel {
    using Type = typename ValueType<Pixel>::Type;
    Type value;

    TMonoPixel(Type value) : value(value)
    {}

    Pixel operator+(const TMonoPixel& other) const {
        return Pixel(value + other.value);
    }
};

struct Mono8Pixel : TMonoPixel<Mono8Pixel> {
    using TMonoPixel::TMonoPixel;
};

The type of Mono8Pixel(2) + Mono8Pixel(2) is Mono8Pixel.

So I guess I'm asking whether these CRTP-based structs have standard layout after all of these changes to the type of value.

They do:

static_assert(std::is_standard_layout_v<Mono8Pixel>);

Complete example: https://godbolt.org/z/8z0CKX

Evg
  • 25,259
  • 5
  • 41
  • 83
  • @Romen, please see the updated answer. To use `Type` from `IntNumber`, you have to use a type trait (like `Value_type` in my example). – Evg Jun 06 '19 at 17:45
  • Thank you, this is very helpful! I am now wondering whether all of these changes are going to affect my use-case for such a class. I am really defining a bunch of pixel structs for mono 8-bit, 16-bit, rgb, rgba, etc. and I am hoping to cast an `unsigned char*` into these various structs for iterating over pixels. Do you see any issues using this pattern for that purpose? – Romen Jun 06 '19 at 17:46
  • @Romen, what do you mean by casting? Not all casts are valid, beware of undefined behavior and strict aliasing rules. – Evg Jun 06 '19 at 17:48
  • @Romen, if I correctly understand your intention, this question can be helpful: https://stackoverflow.com/questions/30617519/reinterpret-cast-from-object-to-first-member – Evg Jun 06 '19 at 17:50
  • If I have `unsigned char* pixelData` and I have a guarantee that there is 32-bits per pixel, I would like to be able to use an `RGBAPixel` struct with `reinterpret_cast(pixelData)` so that I can edit the RGBA channels in the image using these various operators. – Romen Jun 06 '19 at 17:51
  • @Romen, and `RGBA` is an `IntNumber` from your question? – Evg Jun 06 '19 at 17:52
  • `TNumber` in my question would actually be my `TMonoPixel` template and `IntNumber` is my `Mono8Pixel` struct which uses a single `uint8_t` member. The rgb and rgba versions of these structs are going to have their own templates. ... So I guess I'm asking whether these CRTP-based structs have standard layout after all of these changes to the type of `value`. – Romen Jun 06 '19 at 17:56
  • 1
    @Romen, they do. And those with `virtual` functions don't. – Evg Jun 06 '19 at 18:01