Skip to content

Commit

Permalink
ARROW-13296: [C++] Provide a reflection compatible enum replacement
Browse files Browse the repository at this point in the history
Provides an enum replacement with minimal reflection capabilities. These can be declared by inheriting from a helper with CRTP, including a static string literal data member containing the enum's values:

```c++
struct Color : EnumType<Color> {
  using EnumType::EnumType;
  static constexpr char* kValues = "red green blue";
};
```

Values of enumerations declared in this way can be constructed from their string representations at compile time, and can be converted to their string representation for easier debugging/logging/and less repetitive boilerplate in the bindings and elsewhere mapping to/from user provided string values.

For example:
```c++
int get_hex_value() {
    std::string input;
    std::cin >> input;
    switch (*Color(repr)) {
      case *Color("red"):
        return 0xff0000;
      case *Color("green"):
        return 0x00ff00;
      case *Color("blue"):
        return 0x0000ff;
      default:
        std::cout << "Don't know that one; input hex value\n";
        std::cin >> std::hex >> input;
        return std::stoi(input);
    }
}
```

Closes apache#10691 from bkietz/13296-Provide-reflection-compat

Authored-by: Benjamin Kietzman <[email protected]>
Signed-off-by: David Li <[email protected]>
  • Loading branch information
bkietz authored and lidavidm committed Jul 12, 2021
1 parent 9c6d417 commit 975f459
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 0 deletions.
162 changes: 162 additions & 0 deletions cpp/src/arrow/util/enum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

#pragma once

#include <string>
#include <type_traits>
#include <vector>

#include "arrow/util/string_view.h"

namespace arrow {
namespace internal {

constexpr bool IsSpace(char c) { return c == ' ' || c == '\n' || c == '\r'; }

constexpr char ToLower(char c) { return c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c; }

constexpr bool CaseInsensitiveEquals(const char* l, const char* r,
size_t limit = util::string_view::npos) {
return limit == 0
? true
: ToLower(l[0]) != ToLower(r[0])
? false
: l[0] == '\0' ? true : CaseInsensitiveEquals(l + 1, r + 1, limit - 1);
}

constexpr bool CaseInsensitiveEquals(util::string_view l, util::string_view r) {
return l.size() == r.size() && CaseInsensitiveEquals(l.data(), r.data(), l.size());
}

constexpr const char* SkipWhitespace(const char* raw) {
return *raw == '\0' || !IsSpace(*raw) ? raw : SkipWhitespace(raw + 1);
}

constexpr const char* SkipNonWhitespace(const char* raw) {
return *raw == '\0' || IsSpace(*raw) ? raw : SkipNonWhitespace(raw + 1);
}

constexpr size_t TokenSize(const char* token_start) {
return SkipNonWhitespace(token_start) - token_start;
}

constexpr size_t NextTokenStart(const char* raw, size_t token_start) {
return SkipWhitespace(SkipNonWhitespace(raw + token_start)) - raw;
}

template <typename Raw, size_t... Offsets>
struct EnumTypeImpl {
static constexpr int kSize = sizeof...(Offsets);

static constexpr util::string_view kValueStrs[sizeof...(Offsets)] = {
{Raw::kValues + Offsets, TokenSize(Raw::kValues + Offsets)}...};

static constexpr int GetIndex(util::string_view repr, int i = 0) {
return i == kSize
? -1
: CaseInsensitiveEquals(kValueStrs[i], repr) ? i : GetIndex(repr, i + 1);
}
};

template <typename Raw, size_t... Offsets>
constexpr util::string_view const
EnumTypeImpl<Raw, Offsets...>::kValueStrs[sizeof...(Offsets)];

/// \cond false
template <typename Raw, bool IsEnd = false,
size_t MaxOffset = SkipWhitespace(Raw::kValues) - Raw::kValues,
size_t... Offsets>
struct EnumTypeBuilder
: EnumTypeBuilder<Raw, Raw::kValues[NextTokenStart(Raw::kValues, MaxOffset)] == '\0',
NextTokenStart(Raw::kValues, MaxOffset), Offsets..., MaxOffset> {};

template <typename Raw, size_t TerminalNullOffset, size_t... Offsets>
struct EnumTypeBuilder<Raw, /*IsEnd=*/true, TerminalNullOffset, Offsets...> {
using ImplType = EnumTypeImpl<Raw, Offsets...>;
};

// reuse struct as an alias for typename EnumTypeBuilder<Raw>::ImplType
template <typename Raw>
struct EnumTypeImpl<Raw> : EnumTypeBuilder<Raw>::ImplType {};
/// \endcond

struct EnumTypeTag {};

/// \brief An enum replacement with minimal reflection capabilities.
///
/// Declare an enum by inheriting from this helper with CRTP, including a
/// static string literal data member containing the enum's values:
///
/// struct Color : EnumType<Color> {
/// using EnumType::EnumType;
/// static constexpr char* kValues = "red green blue";
/// };
///
/// Ensure the doccomment includes a description of each enum value.
///
/// Values of enumerations declared in this way can be constructed from their string
/// representations at compile time, and can be converted to their string representation
/// for easier debugging/logging/...
template <typename Raw>
struct EnumType : EnumTypeTag {
constexpr EnumType() = default;

constexpr explicit EnumType(int index)
: index{index >= 0 && index < EnumTypeImpl<Raw>::kSize ? index : -1} {}

constexpr explicit EnumType(util::string_view repr)
: index{EnumTypeImpl<Raw>::GetIndex(repr)} {}

constexpr bool operator==(EnumType other) const { return index == other.index; }
constexpr bool operator!=(EnumType other) const { return index != other.index; }

/// Return the string representation of this enum value.
std::string ToString() const {
return EnumTypeImpl<Raw>::kValueStrs[index].to_string();
}

/// \brief Valid enum values will be truthy.
///
/// Invalid enums are constructed with indices outside the range [0, size), with strings
/// not present in EnumType::value_strings(), or by default construction.
constexpr explicit operator bool() const { return index != -1; }

/// Convert this enum value to its integer index.
constexpr int operator*() const { return index; }

/// The number of values in this enumeration.
static constexpr int size() { return EnumTypeImpl<Raw>::kSize; }

/// String representations of each value in this enumeration.
static std::vector<util::string_view> value_strings() {
const util::string_view* begin = EnumTypeImpl<Raw>::kValueStrs;
return {begin, begin + size()};
}

int index = -1;

friend inline void PrintTo(const EnumType& e, std::ostream* os) {
PrintTo(e.ToString(), os);
}
};

template <typename T>
using is_reflection_enum = std::is_base_of<EnumTypeTag, T>;

} // namespace internal
} // namespace arrow
66 changes: 66 additions & 0 deletions cpp/src/arrow/util/reflection_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <gtest/gtest.h>

#include "arrow/util/enum.h"
#include "arrow/util/reflection_internal.h"
#include "arrow/util/string.h"

Expand Down Expand Up @@ -220,5 +221,70 @@ TEST(Reflection, EnumTraits) {
static_assert(std::is_same<EnumTraits<PersonType>::Type, Int8Type>::value, "");
}

TEST(Reflection, CompileTimeStringOps) {
static_assert(CaseInsensitiveEquals("a", "a"), "");
static_assert(CaseInsensitiveEquals("Ab", "ab"), "");
static_assert(CaseInsensitiveEquals("Ab ", "ab", 2), "");
static_assert(CaseInsensitiveEquals(util::string_view{"Ab ", 2}, "ab"), "");

static_assert(CaseInsensitiveEquals(SkipWhitespace(" a"), "a"), "");
static_assert(CaseInsensitiveEquals(SkipWhitespace("a b"), "a b"), "");

static_assert(CaseInsensitiveEquals(SkipNonWhitespace(" a"), " a"), "");
static_assert(CaseInsensitiveEquals(SkipNonWhitespace("a b"), " b"), "");

static_assert(TokenSize("aba ddf") == 3, "");

static_assert(NextTokenStart("aba ddf dfas", 4) == 8, "");
}

/// \brief Enumeration of primary colors.
///
/// - red: Hex value 0xff0000
/// - green: Hex value 0x00ff00
/// - blue: Hex value 0x0000ff
struct Color : EnumType<Color> {
using EnumType<Color>::EnumType;
static constexpr const char* kValues = "red green blue";
};

TEST(Reflection, EnumType) {
static_assert(Color::size() == 3, "");
EXPECT_EQ(Color::value_strings(),
std::vector<util::string_view>({"red", "green", "blue"}));

static_assert(Color("red").index == 0, "");
static_assert(*Color("GREEN") == 1, "");
static_assert(Color("Blue") == Color(2), "");

EXPECT_EQ(Color("red").ToString(), "red");
EXPECT_EQ(Color("GREEN").ToString(), "green");
EXPECT_EQ(Color("Blue").ToString(), "blue");

static_assert(Color("GREEN") == Color("Green"), "");
static_assert(Color("GREEN") == Color(1), "");
static_assert(Color("GREEN") != Color(), "");

static_assert(!Color("chartreuse"), "");
static_assert(Color("violet") == Color(), "");
static_assert(Color(-1) == Color(), "");
static_assert(Color(-29) == Color(), "");
static_assert(Color(12334) == Color(), "");

for (util::string_view repr : {"Red", "orange", "BLUE"}) {
switch (*Color(repr)) {
case* Color("blue"):
EXPECT_EQ(repr, "BLUE");
break;
case* Color("red"):
EXPECT_EQ(repr, "Red");
break;
default:
EXPECT_EQ(repr, "orange");
break;
}
}
}

} // namespace internal
} // namespace arrow

0 comments on commit 975f459

Please sign in to comment.