OutpostUniverse / OP2Utility

C++ library for working with Outpost 2 related files and tasks.
MIT License
4 stars 0 forks source link

Improve tag string initialization #178

Closed DanRStevens closed 5 years ago

DanRStevens commented 6 years ago

I'd like a better way to initialize and store tags used in file processing, such as:

const std::array<char, 4> TagVOL_{ 'V', 'O', 'L', ' ' }; // Volume file tag

In particular, it would be nice if we could use a string literal "VOL ", while also maintaining a strictly controlled struct size of 4 bytes.


I went back to the StackOverflow question about initializing a std::array<char, N> with a string literal, and omitting the trailing null, and did some reading and experimentation. What was posted works, and by adding a few constexpr, I was able to get g++ to do compile time initialization of static const variables. I think this would be really good for what we are doing here.

I'm about to take a break, so for now I'll post my experiment:

#include <array>
#include <utility>
#include <iostream>

template<std::size_t N, std::size_t... IndexSequence>
constexpr std::array<char, N - 1> stripNullTerminator(const char (&string)[N], std::index_sequence<IndexSequence...>) {
  return {{string[IndexSequence]...}};
}

template <std::size_t N>
constexpr std::array<char, N - 1> stripNullTerminator(const char (&string)[N]) {
  return stripNullTerminator(string, std::make_index_sequence<N - 1>());
}

template<std::size_t N>
class NonNullTerminatedString {
public:
  constexpr NonNullTerminatedString(const char (&string)[N + 1]) : string(stripNullTerminator(string)) {}
  operator std::string() const {
    return std::string(string.data(), string.size());
  }
  friend std::ostream& operator << (std::ostream& out, const NonNullTerminatedString& nonNullTerminatedString) {
    return out.write(nonNullTerminatedString.string.data(), nonNullTerminatedString.string.size());
  }
private:
  std::array<char, N> string;
};

using Tag = NonNullTerminatedString<4>;

constexpr auto TagVol_ = Tag("VOL ");
constexpr auto TagVolh = Tag("volh");
constexpr auto TagVols = Tag("vols");
constexpr auto TagVblk = Tag("VBLK");
constexpr std::array<char, 4> tag2 = {'V', 'O', 'L', ' '};
const std::string s2(tag2.data(), tag2.size());

int main() {
  auto a = stripNullTerminator("Some string");
  std::cout << a.size() << std::endl;
  std::cout << s2 << std::endl << std::endl;

  std::cout << TagVol_ << std::endl;
  std::cout << TagVolh << std::endl;
  std::cout << TagVols << std::endl;
  std::cout << TagVblk << std::endl;

  return 0;
}

I'm uncertain if I really want to wrap std::array in a new type. I'm uncertain if I want the template helper methods global or wrapped in a new type or namespace. I'm uncertain if the recursive templates and parameter packs are really necessary, or if there might be a more clear way to implement this. I believe the variables could be declared as const rather than constexpr, though using constexpr may provide additional checks, and may change output.

I may also want to re-read and perhaps summarize some of the relevant articles. In particular: Parameter pack std::integer_sequence and std::index_sequence constexpr LiteralType RIP index_sequence, 2014-2017

Brett208 commented 6 years ago

Thanks for working on this. I've never used friend and I'm not really familiar with constexpr so this effort seems a bit out of my knowledge right now. If we end up just sticking with the const std::array in the end I would be okay, although the easier syntax would be nice.

An intermediate option might be for a using statement to hide the const std::array<char, 4> part. It doesn't solve the awkward initialization char list though.

DanRStevens commented 6 years ago

Hmm, you might be right in that I could probably use a using alias instead of a new class. Might want to shorten the name of the method that creates the std::array in that case though, or provide an alias to it. Perhaps something like:

using makeTag = stripNullTerminator<4>;

static const auto TagVol_ = makeTag("VOL ");

The constexpr is like const, except that it can be computed at compile time rather than run time. In some cases it's an error to pass a non-compile-time value. In other cases, such as on a method definition, you can get compile time constexpr behaviour for literal values, while otherwise it falls back to const and a run time call for non-literal values. Very handy, and lets you do both compile time and run time computations with the same code.

As for friend, all you need to know is that friends are allowed to touch your private members :wink: