Chris_F
Chris_F

Reputation: 5557

How can I handle a class that can take a pointer to either a const or non-const resource?

I'm creating a type punning class View which takes a pointer to byte and adapts it to an array of T. The issue is that a non-const View can be constructed from a const byte*. I don't want to have separate, incompatible types like a View and ConstView. Maybe I could have a member bool readonly that gets set in the const byte* constructor and is checked for in the non-const operator[] overload, causing it to throw. Is there a better way to handle this?

using std::byte;

template <class T>
class View {
public:
    typedef T __attribute__((may_alias)) value_type;
    typedef value_type* pointer;
    typedef const pointer const_pointer;
    typedef value_type& reference;
    typedef const reference const_reference;
    View(byte* p)
        : data { buffer }
    {}
    View(const byte* p)
        : data { const_cast<byte*>(p) }
    {}
    reference operator[](int index) {
        return reinterpret_cast<pointer>(data)[index];
    }
    const_reference operator[](int index) const {
        return reinterpret_cast<const_pointer>(data)[index];
    }
private:
    byte* data;
};

Upvotes: 2

Views: 87

Answers (1)

Silvio Mayolo
Silvio Mayolo

Reputation: 70267

We can actually do all of this checking at compile-time. You're using std::byte, so I assume you're on at least C++17, which means this is really straightforward (We can do a lot of these tricks with older C++ versions, but it involves more template trickery)

We can use static_assert to enable or disable functions depending on the input type. And we'll use is_const_v to check whether our T type is const or not.

template <class T>
class View {
public:
    ...
    View(std::byte* p)
        : data { p } {
      static_assert(!std::is_const_v<T>);
    }
    View(const std::byte* p)
        : data { const_cast<std::byte*>(p) } {
      static_assert(std::is_const_v<T>);
    }
    reference operator[](int index) {
      static_assert(!std::is_const_v<T>);
      return reinterpret_cast<pointer>(data)[index];
    }
    const_reference operator[](int index) const {
      return reinterpret_cast<const_pointer>(data)[index];
    }
private:
    std::byte* data;
};

static_assert is just like assert, except that it runs when the code is generated rather than when it's run. So we define two constructors. One takes an std::byte* and only exists when T is not constant. The other takes a const std::byte* and only exists when T is constant.

Likewise, we have two overloads for operator[]. The first overload returns a mutable reference but can only be used if T is non-const. The second returns a const reference can be used in general. We don't need any assertions for it. (The C++ standard library uses that idiom all over the place: One function returns a constant reference from a const this pointer and one returns a mutable reference, and C++'s overloading rules can handle it)

To use

View<int> x { new std::byte[1] };
View<const int> y { const_cast<const std::byte*>(new std::byte[1]) };

// All fine
x[0] = 100;
std::cout << x[0] << std::endl;
std::cout << y[0] << std::endl;

// Fails at compile time
// y[0] = 100;

return 0;

Also, you'll want to give Rule of Three/Five a thorough read at some point soon. You're taking a pointer as argument, so you need to understand how to manage that resource. You'll either need to (preferred) take a smart pointer rather than a raw one, or if you insist on the raw pointer then you need to write your own or delete the destructor, move and copy constructors, and move and copy assignment operators.

Upvotes: 1

Related Questions