Search code examples
c++templatesembeddedinteger-promotionc++-templates

How to prevent type promotion when using two or more templates when next one implements the previous


I'm writing a C++ logging implementation for an embedded system.The idea is to use templates to declare the message id (type) and enforce the arguments. There are constraints that the code I'm writing is generated and included and used on multiple devices. The idea is to call function as:

Log<LogMessageID>(arg1, arg2...);

Here's where I am:

enum EventId_e {
  EventId_OverVoltageWarning = 0x01,
  EventId_OverCurrentWarning = 0x02,
  EventId_HumidityAndTempRead = 0x03,
};

logging::LoggingInterface logger_interface; // Declare the device specific implementation for sending and populating time

template <typename... Args>
static void LogMessage(const Args&... args) {
  std::array<uint8_t, kLogMessageSize> log_msg = {0};
  uint8_t* current_ptr =
      log_msg.data() + sizeof(uint32_t) + sizeof(uint8_t);  // Offset for time

  uint8_t* arg_ptrs[] = {reinterpret_cast<uint8_t*>(
      const_cast<void*>(static_cast<const void*>(&args)))...};
  size_t arg_sizes[] = {sizeof(Args)...};

  logger_interface.PopulateMsgTime(log_msg); // Put time in log message (first 5 bytes)

  for (size_t i = 0; i < sizeof...(args); ++i) {
    std::memcpy(current_ptr, arg_ptrs[i], arg_sizes[i]);
    current_ptr += arg_sizes[i];
  }

  logger_interface.SendLog(log_msg); // Send log message
}

template <EventId_e EventId, typename... Args>
static void Log(Args... args) {
  LogMessage(static_cast<Args>(args)...);
}

template <>
void Log<EventId_OverVoltageWarning>(
    uint16_t MaxVoltage, uint16_t MinVoltage, int16_t PeakCurrent) {
  LogMessage(MaxVoltage, MinVoltage, PeakCurrent);
}

template <>
void Log<EventId_OverCurrentWarning>(
    uint16_t MaxVoltage, uint16_t MinVoltage, int16_t PeakCurrent) {
  LogMessage(MaxVoltage, MinVoltage, PeakCurrent);
}

template <>
void Log<EventId_HumidityAndTempRead>(uint8_t Humidity, int16_t Temperature) {
  LogMessage(Humidity, Temperature);
} 

The problem I'm running into is two fold:

  • Firstly the value is not enforced when calling Log<EventID>, for instance I can provide uint32_t when the function expects uint16_t and there's no warning or compile error.
  • Secondly if I don't specify or cast specific type of the argument the argument will be promoted to int32.

Here's an example of a working calls:

Log<EventId_OverVoltageWarning>(static_cast<uint16_t>(3200), static_cast<uint16_t>(2900), static_cast<uint16_t>(4200));

or

uint16_t max_voltage = 3200;
uint16_t min_voltage = 2900;
uint16_t peak_current = 4000;

Log<EventId_OverVoltageWarning>(max_voltage, min_voltage, peak_current);

In above code the arg_sizes will be 2 and the array will be populated properly.

However here's an example of non working call:

Log<EventId_OverVoltageWarning>(3200, 2900, 4000);

In above call the values get promoted to int(32bit) and when trying to assemble the Log message in LogMessage the arg_sizes will be 4 instead of 2. Which in turn will segfault the device since it will try writing into space that's not used for that log message array.

How can I get both of my requirements working? Enforcing the argument number and size as well as them propagating to LogMessage properly.


Solution

  • One way could be to remove the catch-all function template:

    template <EventId_e EventId, typename... Args>
    static void Log(Args... args) {
      LogMessage(static_cast<Args>(args)...);
    }
    

    And instead create two function templates matching the signatures of your three specialisations. You don't have to implement them since they should never be used:

    template <EventId_e>
    static void Log(uint16_t, uint16_t, int16_t) = delete;
    
    template <EventId_e>
    static void Log(uint8_t, int16_t) = delete;
    

    Another option could be to replace your primary template and all the specializations with two constrained functions. Here you'd list all the EventId_es that are acceptable non-type template parameters for the function having the signature you want:

    template <EventId_e eid>
        requires(eid == EventId_OverVoltageWarning ||
                 eid == EventId_OverCurrentWarning)
    void Log(uint16_t a, uint16_t b, int16_t c) {
        LogMessage(a, b, c);
    }
    
    template <EventId_e eid>
        requires(eid == EventId_HumidityAndTempRead)
    void Log(uint8_t a, int16_t b) {
        LogMessage(a, b);
    }