0

Given a matrix template class mat<M,N,T> the following member function allows me to efficiently transpose a row vector or a column vector, since they have the same/corresponding memory footprint:

template<int M, int N=M, typename T = double>
struct mat {
    // ...
    template<int Md = M, int Nd = N, typename = std::enable_if_t<Md == 1 || Nd == 1>>
    const mat<N, M, T>& transposedView() const {
        static_assert(M == 1 || N == 1, "transposedView() supports only vectors, not general matrices.");
        return *reinterpret_cast<const mat<N, M, T>*>(this);
    }
}

I've been using this function for years and out of habit started calling it on temporary expressions (/*vector-valued expression*/).transposedView(), forgetting that it would then return a reference to a temporary, and result in undefined behavior by which GCC just bit me.

Is there a simple way for me to add something that would generate some kind of warning when I invoke it on a temporary?

Or is it actually supposed to be safe to call it on a temporary as long as I don't store the reference?

Museful
  • 6,711
  • 5
  • 42
  • 68
  • The strategy has Undefined Behavior. It's luck that it hasn't stopped working or randomly broken your program. – François Andrieux Jul 29 '21 at 01:05
  • You can [overload on lvalueness/rvalueness](https://stackoverflow.com/questions/37977197/confusion-overloading-memberfunctions-on-rvalues-and-lvalues) for class member functions. You can declare and delete an rvalue version. – Nathan Pierson Jul 29 '21 at 01:06
  • @FrançoisAndrieux Even if I don't store the returned reference but only use it immediately? – Museful Jul 29 '21 at 01:07
  • 1
    @Museful Yes. You are creating a pointer to a `mat` where there isn't such an object (unless `M` and `N` are both 1). Then, dereferencing that pointer is a violation of [type aliasing](https://en.cppreference.com/w/cpp/language/reinterpret_cast#Type_aliasing) rules. Basically the compiler will assume the returned pointer points to a different object than `this` because they point to different types. Sometimes, this won't cause problems, but the compiler can break your code's behavior when trying to optimize. – François Andrieux Jul 29 '21 at 01:12
  • @FrançoisAndrieux And if I never invoke the function on a temporary, but only on l-values, is it still UB? – Museful Jul 29 '21 at 01:13
  • 1
    @Museful Yes, dereferencing that `reinterpret_cast` is automatically Undefined Behavior in this case (again, unless `mat` is the same type as `mat`). Generally, if `reinterpret_cast` seems like it solves a problem, it probably doesn't. Most of the time, the only thing you can do with it is cast something back to its true original type. – François Andrieux Jul 29 '21 at 01:17
  • @FrançoisAndrieux `mat` and `mat<1,N,T>` both contain only a `std::array` and no other data members. Is it still UB? – Museful Jul 29 '21 at 01:20
  • Yep. Still UB.`std::complex` has a similar issue since it's common to pass arrays of complex numbers to/from C libraries. So the standard library gets around UB by declaring it not applicable to std::complex is certain common uses. – doug Jul 29 '21 at 01:28
  • Yes. I linked the rules earlier. – François Andrieux Jul 29 '21 at 01:28
  • @FrançoisAndrieux Would it be UB if I use a C-style cast? (I'm wondering why `reinterpret_cast` is even called by that name then.) – Museful Jul 29 '21 at 02:04
  • Still UB. A C style cast will be exactly equivalent to any C++ cast it replaces. You can't save this strategy. – François Andrieux Jul 29 '21 at 02:13
  • @FrançoisAndrieux Thanks for your patience and confirmations. So if I understood correctly, C++ gives me no way to interpret the memory holding one POD as a different POD. – Museful Jul 29 '21 at 02:22
  • @Museful No there is no portable mechanism for that in C++. You have to `memcpy` (if the types are truly POD). A lot of the time the compiler will optimize out the `memcpy`, but I don't think that optimization would be likely in your case. – François Andrieux Jul 29 '21 at 13:14

1 Answers1

3

Member function can be qualified for lvalue or rvalue objects. Using that, you can create an overload set like

template<int M, int N=M, typename T = double>
struct mat {
    // ...
    template<int Md = M, int Nd = N, typename = std::enable_if_t<Md == 1 || Nd == 1>>
    const mat<N, M, T>& transposedView() & const {
        static_assert(M == 1 || N == 1, "transposedView() supports only vectors, not general matrices.");
        return *reinterpret_cast<const mat<N, M, T>*>(this);
    }
    template<int Md = M, int Nd = N, typename = std::enable_if_t<Md == 1 || Nd == 1>>
    const mat<N, M, T>& transposedView() && const = delete;
}

and now if you try to call the function with an rvalue object, you will get a compiler error.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • It turns out even invoking it on l-values is technically UB. But you answered exactly the question I asked. – Museful Jul 29 '21 at 13:17