1

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;
};
Chris_F
  • 4,991
  • 5
  • 33
  • 63
  • If you're willing to deal in type punning, why do you care about `const`-correctness? Also, if it's truly a "view" class, it should *never* provide non-`const` access. A "view" shouldn't be able to modify things. Lastly, what's the point of type-punning at all? If you know that it's an array of `T`, and `T` is a given template... just store a `T*`. – Nicol Bolas Nov 01 '22 at 04:33
  • Also, do you not have `std::span` or `gsl::span` available to you? – Nicol Bolas Nov 01 '22 at 04:45
  • @NicolBolas Would it not be a safe assumption to make that I do in fact intend to both read and modify the data, potentially under multiple interpretations? Perhaps it's debatable whether or not "view" is the best terminology. In e.g. JavaScript the term "view" is used to mean a mutable reinterpretation of a memory buffer. But I digress. – Chris_F Nov 01 '22 at 04:51
  • "*potentially under multiple interpretations*" But a particular `View` instance *itself* only uses 1 interpretation: `T`. The `reinterpret_cast` should happen in the conversion from a `View` to a `View`. – Nicol Bolas Nov 01 '22 at 04:52
  • Does std::span play nice with arbitrary punning? I honestly don't know but my instinct tells me the answer is no. – Chris_F Nov 01 '22 at 04:53
  • `span` doesn't need to know about any punning. You can write a `pun_span` function that takes a `span` and turns it into a `span`. Why does the punning have to happen in the type itself? – Nicol Bolas Nov 01 '22 at 04:54
  • @NicolBolas The point of this class is to be able to take an arbitrary pointer to some data and make it easy to interpret it as an array of something. Like casting a void* to a std::vector. That pointer could be to dynamically allocated memory on the heap. It could be on the stack. It could be static memory. I don't think that is what span is intended for, but if you know of a way of using span to do this simply without any hassle, then I would love to see an example. – Chris_F Nov 01 '22 at 05:02
  • `span` is just a pointer and a size. You just `reinterpret_cast` the pointer before giving it to the `span`. You can even write a function to do that automatically. Or you `reinterpret_cast(span::data())` to make a `span`. It's *really* simple. – Nicol Bolas Nov 01 '22 at 05:14
  • @NicolBolas and how does that play with `__attribute__((may_alias))` to ensure that the compiler's no-strict-aliasing optimizations wont cause errors? – Chris_F Nov 01 '22 at 05:16
  • That would depend on what you're doing, but generally speaking, standard library types don't pretend that parts of the standard don't exist. I assumed you were doing the forms of "punning" that you can reasonably expect to work. However, if this `may_alias` thing is part of a type, you should be able to shove one into the template argument for `span`. – Nicol Bolas Nov 01 '22 at 05:24
  • @NicolBolas I gave it a go here: https://godbolt.org/z/sfK9sb9MG and it did not work. Strict aliasing optimization has borked it. "I assumed you were doing the forms of "punning" that you can reasonably expect to work." No, I'm doing the type of punning that make the compiler want to cry because I'm completely circumventing the type system for fun and profit. – Chris_F Nov 01 '22 at 05:34
  • It may be related to this bug where it looks like attributes like may_alias get stripped from a type when they are passed as template parameters. https://gcc.gnu.org/bugzilla//show_bug.cgi?id=97222 – Chris_F Nov 02 '22 at 00:22

1 Answers1

1

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.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • "*We can use static_assert to enable or disable functions depending on the input type.*" That doesn't enable or disable the function. It causes a compile error [unless your `View`'s lvalue is explicitly `const`](https://gcc.godbolt.org/z/5rTdba6vr). What you want is to employ SFINAE. – Nicol Bolas Nov 01 '22 at 04:42
  • Seem weird to be having a `View` rather than a `const View`. – Chris_F Nov 01 '22 at 04:45