Search code examples
c++templatestemplate-specializationauto

When is a return type not exactly a return type? (template specialization, "auto")


TL;DR - Why does it seem my one method isn't returning the type it says it does and thus causes issues with my specialized template methods?

This is a case of I'm not 100% sure what I'm dealing with, so I'm not sure how to title the question properly. I have two classes... Union32 is a class which provides endian-correct access to portions of a multi-byte/multi-word scalar (the class here has been chopped down for example purposes). The class contains a couple of template methods, Get() and Set(), and specializations of each are provided.

Modbus32 is a template class for a 32-bit scalar stored in a Modbus register file; Modbus registers are 16-bit, so concatenating two permits storing/accessing 32-bit values. But since Modbus is a big-endian protocol (16-bit registers are transmitted MSB first), you almost certainly want to store the most-significant word first; Modbus32 encapsulates that behavior and uses Union32 to re-assemble the 32-bit value according to the machine's endianness.

Modbus32 also has Get() and Set() methods, along with a GetUnion() method which returns a Union32 object assembled from the uint16_t msw and uint16_t lsw class members. The parameter to the Modbus32 template is a 32-bit typename (e.g. uint32_t, int32_t), and this typename (ValueType) is used to govern which specialized Union32::Get() method is called when returning the value of the 32-bit Modbus register.

Anyway, to the example, and the errors I encountered while trying to create Modbus32::Get() (link to example hosted on Coliru, using C++14):

#include <iostream>
#include <string>
#include <type_traits>
#include <cstdint>

#define INDUCE_COMPILER_ERRORS   0   // Set to 1 to see the compiler errors.

/* Container for 32-bit data, allows endian-correct access to bytes/words. */
class Union32 {
 public:
  Union32() { value = 0; }
  explicit Union32(uint16_t msw, uint16_t lsw) {
    endian_word.lsw = lsw;
    endian_word.msw = msw;
  }
  static constexpr size_t kLength = 4;
  template<typename ValueType> ValueType Get(void) const;
  template<typename ValueType> void Set(ValueType set_value);
  /* Machine is little endian. */
  struct endian_word_t {
    uint16_t lsw;           // Least-significant word.
    uint16_t msw;           // Most-significant word.
  };
  union {
    uint32_t value;             // Integer value.
    int32_t signed_value;       // Signed integer value.
    endian_word_t endian_word;  // Least/most significant 16-bit words.
  };
};

/* Specializations for Union32::Get(). */
template<> inline int32_t Union32::Get<int32_t>(void) const { return signed_value; }
template<> inline uint32_t Union32::Get<uint32_t>(void) const { return value; }
template<> inline void Union32::Set<int32_t>(int32_t set_value) { signed_value = set_value; }
template<> inline void Union32::Set<uint32_t>(uint32_t set_value) { value = set_value; }

/* Class that explicitly stores MSW first in memory, a la Modbus. */
template<typename ValueType,
    class = typename std::enable_if<std::is_arithmetic<ValueType>::value>::type,
    class = typename std::enable_if<sizeof(ValueType) == sizeof(uint32_t)>::type>
struct Modbus32 {
  uint16_t msw;       // Most significant word.
  uint16_t lsw;       // Least significant word.
  inline Union32 GetUnion(void) const {
    return Union32(msw, lsw);  // Creates a Union32 object from the MSW and LSW.
  }
  inline ValueType Get_Working1(void) const {
    Union32 u32 = GetUnion();
    return u32.Get<ValueType>();
  }
  inline ValueType Get_Broken1(void) const {
    /* Error: no matching function for call to 'Union32::Get()' */
    /* Only generates an error when called by conversion operator. */
    return GetUnion().Get();
  }
#if INDUCE_COMPILER_ERRORS   // Set to 1 to see the compiler errors.
  inline ValueType Get_Broken2(void) const {
    /* Error: expected primary-expression before '>' token */
    return GetUnion().Get<ValueType>(); // doesn't like the explicit template parameter, I guess?
  }
  inline ValueType Get_Broken3(void) const {
    auto u32 = GetUnion();
    /* Error: expected primary-expression before '>' token */
    return u32.Get<ValueType>(); // only difference from Get_Working1() is "Union32" vs. "auto"
  }
#endif
  inline operator ValueType() const {
    /* Call one of the above Get() methods here. */
    return Get_Working1();
    //return Get_Broken1();  // will cause compiler error when invoked
  }
};

typedef Modbus32<uint32_t> ModbusUint32;
typedef Modbus32<int32_t> ModbusInt32;

int main()
{
    std::cout << "Hello from GCC " __VERSION__ "!" << std::endl;
    ModbusUint32 foo32;
    foo32.msw = 2;             // Returns 2*65536 + 1*1 = 131073
    foo32.lsw = 1;
    uint32_t value = foo32;    // Calls the implicit conversion operator.
    std::cout << "value = " << value << std::endl;
    return 0;
}

I have found exactly one way to compose the Get() method such that the compiler likes it, namely Get_Working1(). This method declares a local Union32 u32 and assigns it the result of the GetUnion() method, then in the next statement it returns the value u32.Get<ValueType>(). That seems pretty straightforward.

However, I've found three different ways to break that working example, using methods Get_Broken1/2/3(). Get_Broken1() may be a red herring, as I would expect template argument deduction shouldn't work here, so probably ignore that one for now. But the other two don't make sense to me, and I have a suspicion it comes down to the return type (or possibly rvalue/lvalue-ness) of GetUnion().

Get_Broken2() takes the working method and removes the u32 local variable (an lvalue), using GetUnion() as an rvalue instead. Get_Broken3() changes even less, only substituting auto for Union32 when declaring u32. And yet both of these methods break the compiler (expects primary expression, etc), despite appearing to be functionally equivalent to Get_Working1().

The return type for GetUnion() is Union32. So why can't I call GetUnion().Get<ValueType>() without protest? I thought maybe it was an lvalue/rvalue thing, but the third example I think disproves that and tells me that there's something else going on. In Get_Broken3(), how does auto u32 = GetUnion(); not result in u32 being of type Union32? Why does it matter if u32 is explicitly declared to be Union32 instead of automatically? How could u32 be anything other than Union32?

In summary, I have working code in Get_Working1(), and while I've managed to break the compiler with three different examples, I think Get_Broken3() probably exposes the true root of the issue. Unfortunately, I have no idea what that is. If someone can shed some light on this, that would be tremendous, thanks.


Solution

  • Both gcc and clang think that the return value of GetUnion is dependent onnthe type parameters of Modbus. I don;t know why.

    To fix the compile error, add template before Get.

    Because GetUnion().Get is being parsed as if Get was a value in the struct returned from GetUnion().

    In the working example, Union32 x=GetUnion();, the type of x is not considered dependent on the template arguments. It is Union32.

    I am uncertain why the compilers consider the return type of a method with a fixed return type as dependent on the template parameters within the body of the class definition. It might just be a slightly screwed up standard C++ rule.

    So, try:

    return GetUnion().template Get<ValueType>();