Home > front end >  A problem with CRTP, containers and pointers
A problem with CRTP, containers and pointers

Time:01-06

I have this CRTP base class:

template <class Child>
    class Container {
            friend Child;
            Container() {};
        public:
            decltype(auto) begin() { return static_cast<const Child&>(*this).abegin(); }
            decltype(auto) end() { return static_cast<Child&>(*this).aend(); }
            decltype(auto) begin() const { return static_cast<const Child&>(*this).abegin(); }
            decltype(auto) end() const { return static_cast<const Child&>(*this).aend(); }
};

And now, an array, trying to be the Child of that container:

template<typename T, size_t N>
    class Array: public Container<Array<T, N>> {
        public:
            T array[N];
        public:
            using iterator = zero::iterator::input_iter<T>;
            using const_iterator = zero::iterator::input_iter<const T>;

            // Iterator stuff
            iterator abegin() { return iterator(&array[0]); }
            iterator aend() { return iterator(&array[N]); }
            constexpr const_iterator abegin() const { return const_iterator(&array[0]); }
            constexpr const_iterator aend() const { return const_iterator(&array[N]); }

            /**
             * @brief returns the number of elements stored in the underlying array
             */
            [[nodiscard]]
            inline consteval int size() const noexcept { return N; }

            template <typename... InitValues>
            Array(InitValues... init_values) 
                : array{ init_values... } {}

    // code goes on...

Here it's the usage:

import std;
import zero;
import collections;
import iterator;
import container;
import type_info;

using namespace zero;

int main() {
    constexpr collections::Array a = collections::Array<long, 5>{1L, 2L, 3L, 4L, 5L};
    Container b = collections::Array<long, 5>{1L, 2L, 3L, 4L, 5L};

    std::cout << "Iterating over the values of a constexpr zero::collection!" << std::endl;
    std::cout << "decltype a: " << zero::types::type_name<decltype(a)>() << std::endl;
    
    for (long value : a)
        std::cout << " - [constexpr] Value: " << value << std::endl;

    std::cout << "Iterating over the values of a zero::container!" << std::endl;
    std::cout << "decltype a: " << zero::types::type_name<decltype(b)>() << std::endl;
    for (long value : b)
        std::cout << " - Value: " << value << std::endl;

    return 0;
}

And the results:

Iterating over the values of a constexpr zero::collection!
decltype a: const zero::collections::Array<long, 5>
 - [constexpr] Value: 1
 - [constexpr] Value: 2
 - [constexpr] Value: 3
 - [constexpr] Value: 4
 - [constexpr] Value: 5

Iterating over the values of a zero::container!
decltype a: zero::Container<zero::collections::Array<long, 5>>
 - Value: 8
 - Value: 0
 - Value: 1
 - Value: 2
 - Value: 3

Well, I just got UB!

I am missunderstading things, pretty sure, but I am not able to understand what I am writting and doing bad.

  • I want to be able to use Container as a interface type in type definitions, functions parameters... typical places without dynamic polimorphism, just using CRTP.

Is someone able to spot my mistakes and help me to fix them?

EDIT: As asked here is the live example on Godbolt

CodePudding user response:

The immediate reason of the UB is object slicing. The type of b is Container<Array<long, 5>> — and it is a complete object. There is no Array<long, 5> around to be a base class subobject of. The temporary collections::Array<long, 5>{1L, 2L, 3L, 4L, 5L} isn't one. So every cast inside member functions of this particular container is UB.

In order to fix it, you need b to be a reference to a Container. Let's try:

const Container& b = collections::Array<long, 5>{1L, 2L, 3L, 4L, 5L};

Oops! This doesn't compile. The reason why you was able to write just Container is CTAD, and CTAD doesn't work for references or pointers or anything but the class template itself. Container is not a type and cannot be used as such. It is a template. So not having CTAD on your side, you need this:

const Container<collections::Array<long, 5>> & b = 
   collections::Array<long, 5>{1L, 2L, 3L, 4L, 5L};

(const because it's a reference to a temporary)

Now this doesn't look as attractive as just Container, does it? And if you need to spell out collections::Array<long, 5>, you could just as well write

const collections::Array<long, 5> & b = 
   collections::Array<long, 5>{1L, 2L, 3L, 4L, 5L};

which is shorter and contains just as much information about b.

Now why do you need Container again?

  • Related