Saturday 12 March 2022

Using Const Char* in Template

Compile Time Template Strings.... Yeah, these are a thing and I'm going to show you a live example of how to do them.  First lets take a moment to just understand what we want in some C++ pseudo code.

template<typename T>
T Square(const T value)
{
   return value * value;
}

const auto result = Square<int>(2);

const auto result = Square<double>(2.42);

In a template like this we can see this requires a number and more advanced writers might use SFINAE and type traits to ensure that the type is an arithmetic type, this would stop us doing something like...

const auto result = Square<"Hello">(2);

In this case we can not even compile, not because "Hello" is not operable with the multiplication, but that it's not that, the issue is that a const char* isn't allowed in the template itself.  You can't define a typename as const char*... Or can you?

Well, it seems you can in C++20.  The idea I had was to accept the template but treat it like a decaying length, copying the buffer in the template each step.  How could I do this?  Well, I need to compare the template somehow and determine the length of "Hello"... The buffer containing the data "Hello\0" itself is there in the code, what I needed was to think about how to treat that as type 6, the length...

The spaceship operator is what sprang to mind, but then I had the issue that the comparison result type wasn't known.... At least I don't know it, I don't know what string literal maybe being passed in, how to know it?  Well I didn't need to, I just return auto!

Here is the key code....  

  template<std::size_t Length>
  struct TemplateString
  {
    // Constructor is constexpr and simply copies the payload text
    // into the member data setup
    constexpr TemplateString(const char(&pPayloadText)[Length + 1])
    {
      std::copy_n(pPayloadText, Length+1, mText);
    }
    
    auto operator<=>(const TemplateString&) const = default;
    
    // Default is just an empty buffer of the Length+1
    char mText[Length+1] = {};
  };
  
  // Make decaying the template Length possible
  template<std::size_t Length>
  TemplateString(const char(&str)[Length])->TemplateString<Length-1>;


The template string is simply copying the buffer given locally, and the unwinding is stopped when the template length is met in the comparator.  I think this is a little strange, I may even be wrong (probably am) in explaining how this unrolling works.

However, what use do I have for this?

Well, my main use is in parameterising on the template the NAME of a thing... I have long seen code where the onus is on the engineer to put some static constant string into their derived class to allow some other feature to know what your class is called, or what it does.

Systems, Events, Services... These sort of things, general things that need a name for us mere humans all introduce this pattern of:

Base Class --> Derived Class :: sName

Where this sName is brittle, it can be forgotten, it can be mangled, it can be copy I paste... All sorts of issues with it.  I personally have my statemachine code up on github and I use an event within that, which could sorely use this very string trick... so I will be using it.

Lets take a look at the code in action on CompilerExplorer here.

And you can get hold of my personal example on github here.

As ever, if you liked this, subscribe on YouTube to let me know, comment below and if you use this trick feel free to buy me a coffee with the tip jar up top of this page.

No comments:

Post a Comment