From 54f2a4c869770c4e800f654bd4f916cdcf898864 Mon Sep 17 00:00:00 2001 From: Tom Jackson Date: Thu, 7 Apr 2016 14:01:28 -0700 Subject: [PATCH] Extensibility for folly::to<> through ADL Summary: Primarily to support slightly more flexible implementations of `split()`; Reviewed By: ot Differential Revision: D3116763 fb-gh-sync-id: 69023c0f26058516f25b9c1f9824055efc7021f9 fbshipit-source-id: 69023c0f26058516f25b9c1f9824055efc7021f9 --- folly/Conv.cpp | 104 ++++++++++++++ folly/Conv.h | 281 +++++++++++++++++--------------------- folly/String-inl.h | 89 ++++-------- folly/String.cpp | 3 +- folly/String.h | 80 ++++++++--- folly/test/ConvTest.cpp | 55 ++++++-- folly/test/StringTest.cpp | 32 ++++- 7 files changed, 389 insertions(+), 255 deletions(-) diff --git a/folly/Conv.cpp b/folly/Conv.cpp index 3458f760..7c6afcff 100644 --- a/folly/Conv.cpp +++ b/folly/Conv.cpp @@ -266,6 +266,110 @@ bool str_to_bool(StringPiece* src) { return result; } +namespace { +/** + * StringPiece to double, with progress information. Alters the + * StringPiece parameter to munch the already-parsed characters. + */ +template +Tgt str_to_floating(StringPiece* src) { + using namespace double_conversion; + static StringToDoubleConverter + conv(StringToDoubleConverter::ALLOW_TRAILING_JUNK + | StringToDoubleConverter::ALLOW_LEADING_SPACES, + 0.0, + // return this for junk input string + std::numeric_limits::quiet_NaN(), + nullptr, nullptr); + + FOLLY_RANGE_CHECK_STRINGPIECE(!src->empty(), + "No digits found in input string", *src); + + int length; + auto result = conv.StringToDouble(src->data(), + static_cast(src->size()), + &length); // processed char count + + if (!std::isnan(result)) { + src->advance(length); + return result; + } + + for (;; src->advance(1)) { + if (src->empty()) { + throw std::range_error("Unable to convert an empty string" + " to a floating point value."); + } + if (!isspace(src->front())) { + break; + } + } + + // Was that "inf[inity]"? + if (src->size() >= 3 && toupper((*src)[0]) == 'I' + && toupper((*src)[1]) == 'N' && toupper((*src)[2]) == 'F') { + if (src->size() >= 8 && + toupper((*src)[3]) == 'I' && + toupper((*src)[4]) == 'N' && + toupper((*src)[5]) == 'I' && + toupper((*src)[6]) == 'T' && + toupper((*src)[7]) == 'Y') { + src->advance(8); + } else { + src->advance(3); + } + return std::numeric_limits::infinity(); + } + + // Was that "-inf[inity]"? + if (src->size() >= 4 && toupper((*src)[0]) == '-' + && toupper((*src)[1]) == 'I' && toupper((*src)[2]) == 'N' + && toupper((*src)[3]) == 'F') { + if (src->size() >= 9 && + toupper((*src)[4]) == 'I' && + toupper((*src)[5]) == 'N' && + toupper((*src)[6]) == 'I' && + toupper((*src)[7]) == 'T' && + toupper((*src)[8]) == 'Y') { + src->advance(9); + } else { + src->advance(4); + } + return -std::numeric_limits::infinity(); + } + + // "nan"? + if (src->size() >= 3 && toupper((*src)[0]) == 'N' + && toupper((*src)[1]) == 'A' && toupper((*src)[2]) == 'N') { + src->advance(3); + return std::numeric_limits::quiet_NaN(); + } + + // "-nan"? + if (src->size() >= 4 && + toupper((*src)[0]) == '-' && + toupper((*src)[1]) == 'N' && + toupper((*src)[2]) == 'A' && + toupper((*src)[3]) == 'N') { + src->advance(4); + return -std::numeric_limits::quiet_NaN(); + } + + // All bets are off + throw std::range_error("Unable to convert \"" + src->toString() + + "\" to a floating point value."); +} + +} + +float str_to_float(StringPiece* src) { + return str_to_floating(src); +} + +double str_to_double(StringPiece* src) { + return str_to_floating(src); +} + /** * String represented as a pair of pointers to char to unsigned * integrals. Assumes NO whitespace before or after, and also that the diff --git a/folly/Conv.h b/folly/Conv.h index b730857b..ac2f5f2b 100644 --- a/folly/Conv.h +++ b/folly/Conv.h @@ -253,10 +253,26 @@ inline uint32_t digits10(uint64_t v) { // This is 20 * 8 == 160 bytes, which fits neatly into 5 cache lines // (assuming a cache line size of 64). static const uint64_t powersOf10[20] FOLLY_ALIGNED(64) = { - 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, - 10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000, - 1000000000000000, 10000000000000000, 100000000000000000, - 1000000000000000000, 10000000000000000000UL + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, + 100000000000, + 1000000000000, + 10000000000000, + 100000000000000, + 1000000000000000, + 10000000000000000, + 100000000000000000, + 1000000000000000000, + 10000000000000000000UL, }; // "count leading zeroes" operation not valid; for 0; special case this. @@ -781,6 +797,20 @@ toAppendDelimStrImpl(const Delimiter& delim, const T& v, const Ts&... vs) { * the space for them and will depend on strings exponential growth. * If you just append once consider using toAppendFit which reserves * the space needed (but does not have exponential as a result). + * + * Custom implementations of toAppend() can be provided in the same namespace as + * the type to customize printing. estimateSpaceNeed() may also be provided to + * avoid reallocations in toAppendFit(): + * + * namespace other_namespace { + * + * template + * void toAppend(const OtherType&, String* out); + * + * // optional + * size_t estimateSpaceNeeded(const OtherType&); + * + * } */ template typename std::enable_if= 3 @@ -957,23 +987,25 @@ namespace detail { }; bool str_to_bool(StringPiece* src); + float str_to_float(StringPiece* src); + double str_to_double(StringPiece* src); template Tgt digits_to(const char* b, const char* e); extern template unsigned char digits_to(const char* b, - const char* e); + const char* e); extern template unsigned short digits_to(const char* b, - const char* e); + const char* e); extern template unsigned int digits_to(const char* b, - const char* e); + const char* e); extern template unsigned long digits_to(const char* b, - const char* e); + const char* e); extern template unsigned long long digits_to( const char* b, const char* e); #if FOLLY_HAVE_INT128_T extern template unsigned __int128 digits_to(const char* b, - const char* e); + const char* e); #endif } // namespace detail @@ -1017,23 +1049,13 @@ to(const char * b, const char * e) { return result; } -/** - * Parsing strings to integrals. These routines differ from - * to(string) in that they take a POINTER TO a StringPiece - * and alter that StringPiece to reflect progress information. - */ - +namespace detail { /** * StringPiece to integrals, with progress information. Alters the * StringPiece parameter to munch the already-parsed characters. */ template -typename std::enable_if< - std::is_integral::value - && !std::is_same::type, bool>::value, - Tgt>::type -to(StringPiece * src) { - +Tgt str_to_integral(StringPiece* src) { auto b = src->data(), past = src->data() + src->size(); for (;; ++b) { FOLLY_RANGE_CHECK_STRINGPIECE(b < past, @@ -1080,158 +1102,58 @@ to(StringPiece * src) { return result; } -/** - * StringPiece to bool, with progress information. Alters the - * StringPiece parameter to munch the already-parsed characters. - */ -template -typename std::enable_if< - std::is_same::type, bool>::value, - Tgt>::type -to(StringPiece * src) { - return detail::str_to_bool(src); -} - -namespace detail { - /** * Enforce that the suffix following a number is made up only of whitespace. */ -inline void enforceWhitespace(const char* b, const char* e) { - for (; b != e; ++b) { - FOLLY_RANGE_CHECK_BEGIN_END(isspace(*b), - to("Non-whitespace: ", *b), - b, e); +inline void enforceWhitespace(StringPiece sp) { + for (char ch : sp) { + FOLLY_RANGE_CHECK_STRINGPIECE( + isspace(ch), to("Non-whitespace: ", ch), sp); } } -} // namespace detail - -/** - * String or StringPiece to integrals. Accepts leading and trailing - * whitespace, but no non-space trailing characters. - */ -template -typename std::enable_if< - std::is_integral::value, - Tgt>::type -to(StringPiece src) { - Tgt result = to(&src); - detail::enforceWhitespace(src.data(), src.data() + src.size()); - return result; -} - /******************************************************************************* * Conversions from string types to floating-point types. ******************************************************************************/ + +} // namespace detail + /** - * StringPiece to double, with progress information. Alters the + * StringPiece to bool, with progress information. Alters the * StringPiece parameter to munch the already-parsed characters. */ -template -inline typename std::enable_if< - std::is_floating_point::value, - Tgt>::type -to(StringPiece *const src) { - using namespace double_conversion; - static StringToDoubleConverter - conv(StringToDoubleConverter::ALLOW_TRAILING_JUNK - | StringToDoubleConverter::ALLOW_LEADING_SPACES, - 0.0, - // return this for junk input string - std::numeric_limits::quiet_NaN(), - nullptr, nullptr); - - FOLLY_RANGE_CHECK_STRINGPIECE(!src->empty(), - "No digits found in input string", *src); - - int length; - auto result = conv.StringToDouble(src->data(), - static_cast(src->size()), - &length); // processed char count - - if (!std::isnan(result)) { - src->advance(length); - return result; - } - - for (;; src->advance(1)) { - if (src->empty()) { - throw std::range_error("Unable to convert an empty string" - " to a floating point value."); - } - if (!isspace(src->front())) { - break; - } - } - - // Was that "inf[inity]"? - if (src->size() >= 3 && toupper((*src)[0]) == 'I' - && toupper((*src)[1]) == 'N' && toupper((*src)[2]) == 'F') { - if (src->size() >= 8 && - toupper((*src)[3]) == 'I' && - toupper((*src)[4]) == 'N' && - toupper((*src)[5]) == 'I' && - toupper((*src)[6]) == 'T' && - toupper((*src)[7]) == 'Y') { - src->advance(8); - } else { - src->advance(3); - } - return std::numeric_limits::infinity(); - } - - // Was that "-inf[inity]"? - if (src->size() >= 4 && toupper((*src)[0]) == '-' - && toupper((*src)[1]) == 'I' && toupper((*src)[2]) == 'N' - && toupper((*src)[3]) == 'F') { - if (src->size() >= 9 && - toupper((*src)[4]) == 'I' && - toupper((*src)[5]) == 'N' && - toupper((*src)[6]) == 'I' && - toupper((*src)[7]) == 'T' && - toupper((*src)[8]) == 'Y') { - src->advance(9); - } else { - src->advance(4); - } - return -std::numeric_limits::infinity(); - } +inline void parseTo(StringPiece* src, bool& out) { + out = detail::str_to_bool(src); +} - // "nan"? - if (src->size() >= 3 && toupper((*src)[0]) == 'N' - && toupper((*src)[1]) == 'A' && toupper((*src)[2]) == 'N') { - src->advance(3); - return std::numeric_limits::quiet_NaN(); - } +/** + * Parsing strings to numeric types. These routines differ from + * parseTo(str, numeric) routines in that they take a POINTER TO a StringPiece + * and alter that StringPiece to reflect progress information. + */ +template +typename std::enable_if< + std::is_integral::type>::value>::type +parseTo(StringPiece* src, Tgt& out) { + out = detail::str_to_integral(src); +} - // "-nan"? - if (src->size() >= 4 && - toupper((*src)[0]) == '-' && - toupper((*src)[1]) == 'N' && - toupper((*src)[2]) == 'A' && - toupper((*src)[3]) == 'N') { - src->advance(4); - return -std::numeric_limits::quiet_NaN(); - } +inline void parseTo(StringPiece* src, float& out) { + out = detail::str_to_float(src); +} - // All bets are off - throw std::range_error("Unable to convert \"" + src->toString() - + "\" to a floating point value."); +inline void parseTo(StringPiece* src, double& out) { + out = detail::str_to_double(src); } -/** - * Any string, const char*, or StringPiece to double. - */ template typename std::enable_if< - std::is_floating_point::value, - Tgt>::type -to(StringPiece src) { - Tgt result = Tgt(to(&src)); - detail::enforceWhitespace(src.data(), src.data() + src.size()); - return result; + std::is_floating_point::value || + std::is_integral::type>::value>::type +parseTo(StringPiece src, Tgt& out) { + parseTo(&src, out); + detail::enforceWhitespace(src); } /******************************************************************************* @@ -1267,6 +1189,59 @@ to(const Src & value) { return result; } +/******************************************************************************* + * Custom Conversions + * + * Any type can be used with folly::to by implementing parseTo. The + * implementation should be provided in the namespace of the type to facilitate + * argument-dependent lookup: + * + * namespace other_namespace { + * void parseTo(::folly::StringPiece, OtherType&); + * } + ******************************************************************************/ +template +typename std::enable_if::value>::type +parseTo(StringPiece in, T& out) { + typename std::underlying_type::type tmp; + parseTo(in, tmp); + out = static_cast(tmp); +} + +inline void parseTo(StringPiece in, StringPiece& out) { + out = in; +} + +inline void parseTo(StringPiece in, std::string& out) { + out.clear(); + out.append(in.data(), in.size()); +} + +inline void parseTo(StringPiece in, fbstring& out) { + out.clear(); + out.append(in.data(), in.size()); +} + +/** + * String or StringPiece to target conversion. Accepts leading and trailing + * whitespace, but no non-space trailing characters. + */ + +template +typename std::enable_if::value, Tgt>::type +to(StringPiece src) { + Tgt result; + parseTo(src, result); + return result; +} + +template +Tgt to(StringPiece* src) { + Tgt result; + parseTo(src, result); + return result; +} + /******************************************************************************* * Enum to anything and back ******************************************************************************/ diff --git a/folly/String-inl.h b/folly/String-inl.h index e3ad12b4..1d613e9c 100644 --- a/folly/String-inl.h +++ b/folly/String-inl.h @@ -263,29 +263,6 @@ inline char delimFront(StringPiece s) { return *s.start(); } -/* - * These output conversion templates allow us to support multiple - * output string types, even when we are using an arbitrary - * OutputIterator. - */ -template struct OutputConverter {}; - -template<> struct OutputConverter { - std::string operator()(StringPiece sp) const { - return sp.toString(); - } -}; - -template<> struct OutputConverter { - fbstring operator()(StringPiece sp) const { - return sp.toFbstring(); - } -}; - -template<> struct OutputConverter { - StringPiece operator()(StringPiece sp) const { return sp; } -}; - /* * Shared implementation for all the split() overloads. * @@ -304,11 +281,9 @@ void internalSplit(DelimT delim, StringPiece sp, OutputIterator out, const size_t strSize = sp.size(); const size_t dSize = delimSize(delim); - OutputConverter conv; - if (dSize > strSize || dSize == 0) { if (!ignoreEmpty || strSize > 0) { - *out++ = conv(sp); + *out++ = to(sp); } return; } @@ -323,7 +298,7 @@ void internalSplit(DelimT delim, StringPiece sp, OutputIterator out, for (size_t i = 0; i <= strSize - dSize; ++i) { if (atDelim(&s[i], delim)) { if (!ignoreEmpty || tokenSize > 0) { - *out++ = conv(StringPiece(&s[tokenStartPos], tokenSize)); + *out++ = to(sp.subpiece(tokenStartPos, tokenSize)); } tokenStartPos = i + dSize; @@ -335,7 +310,7 @@ void internalSplit(DelimT delim, StringPiece sp, OutputIterator out, } tokenSize = strSize - tokenStartPos; if (!ignoreEmpty || tokenSize > 0) { - *out++ = conv(StringPiece(&s[tokenStartPos], tokenSize)); + *out++ = to(sp.subpiece(tokenStartPos, tokenSize)); } } @@ -344,36 +319,25 @@ template StringPiece prepareDelim(const String& s) { } inline char prepareDelim(char c) { return c; } -template -struct convertTo { - template - static Dst from(const Src& src) { return folly::to(src); } - static Dst from(const Dst& src) { return src; } -}; - -template -typename std::enable_if::value, bool>::type -splitFixed(const Delim& delimiter, - StringPiece input, - OutputType& out) { +template +bool splitFixed(const Delim& delimiter, StringPiece input, OutputType& output) { + static_assert( + exact || std::is_same::value || + IsSomeString::value, + "split() requires that the last argument be a string type"); if (exact && UNLIKELY(std::string::npos != input.find(delimiter))) { return false; } - out = convertTo::from(input); + parseTo(input, output); return true; } -template -typename std::enable_if::value, bool>::type -splitFixed(const Delim& delimiter, - StringPiece input, - OutputType& outHead, - OutputTypes&... outTail) { +template +bool splitFixed( + const Delim& delimiter, + StringPiece input, + OutputType& outHead, + OutputTypes&... outTail) { size_t cut = input.find(delimiter); if (UNLIKELY(cut == std::string::npos)) { return false; @@ -382,7 +346,7 @@ splitFixed(const Delim& delimiter, StringPiece tail(input.begin() + cut + detail::delimSize(delimiter), input.end()); if (LIKELY(splitFixed(delimiter, tail, outTail...))) { - outHead = convertTo::from(head); + parseTo(head, outHead); return true; } return false; @@ -429,20 +393,13 @@ void splitTo(const Delim& delimiter, ignoreEmpty); } -template -typename std::enable_if::value, bool>::type -split(const Delim& delimiter, - StringPiece input, - OutputType& outHead, - OutputTypes&... outTail) { +template +typename std::enable_if< + AllConvertible::value && sizeof...(OutputTypes) >= 1, + bool>::type +split(const Delim& delimiter, StringPiece input, OutputTypes&... outputs) { return detail::splitFixed( - detail::prepareDelim(delimiter), - input, - outHead, - outTail...); + detail::prepareDelim(delimiter), input, outputs...); } namespace detail { diff --git a/folly/String.cpp b/folly/String.cpp index 43404026..20634a7a 100644 --- a/folly/String.cpp +++ b/folly/String.cpp @@ -306,8 +306,7 @@ double prettyToDouble(folly::StringPiece *const prettyString, double prettyToDouble(folly::StringPiece prettyString, const PrettyType type){ double result = prettyToDouble(&prettyString, type); - detail::enforceWhitespace(prettyString.data(), - prettyString.data() + prettyString.size()); + detail::enforceWhitespace(prettyString); return result; } diff --git a/folly/String.h b/folly/String.h index 5a5763ca..78e946c6 100644 --- a/folly/String.h +++ b/folly/String.h @@ -432,28 +432,28 @@ template void split(const Delim& delimiter, const String& input, std::vector& out, - bool ignoreEmpty = false); + const bool ignoreEmpty = false); template void split(const Delim& delimiter, const String& input, folly::fbvector& out, - bool ignoreEmpty = false); + const bool ignoreEmpty = false); template void splitTo(const Delim& delimiter, const String& input, OutputIterator out, - bool ignoreEmpty = false); + const bool ignoreEmpty = false); /* * Split a string into a fixed number of string pieces and/or numeric types - * by delimiter. Any numeric type that folly::to<> can convert to from a - * string piece is supported as a target. Returns 'true' if the fields were - * all successfully populated. Returns 'false' if there were too few fields - * in the input, or too many fields if exact=true. Casting exceptions will - * not be caught. + * by delimiter. Conversions are supported for any type which folly:to<> can + * target, including all overloads of parseTo(). Returns 'true' if the fields + * were all successfully populated. Returns 'false' if there were too few + * fields in the input, or too many fields if exact=true. Casting exceptions + * will not be caught. * * Examples: * @@ -481,21 +481,57 @@ void splitTo(const Delim& delimiter, * Note that this will likely not work if the last field's target is of numeric * type, in which case folly::to<> will throw an exception. */ +template +struct IsSomeVector { + enum { value = false }; +}; + +template +struct IsSomeVector, void> { + enum { value = true }; +}; + template -using IsSplitTargetType = std::integral_constant::value || - std::is_same::value || - IsSomeString::value>; - -template -typename std::enable_if::value, bool>::type -split(const Delim& delimiter, - StringPiece input, - OutputType& outHead, - OutputTypes&... outTail); +struct IsSomeVector, void> { + enum { value = true }; +}; + +template +struct IsConvertible { + enum { value = false }; +}; + +template +struct IsConvertible< + T, + decltype(parseTo(std::declval(), std::declval()))> { + enum { value = true }; +}; + +template +struct AllConvertible; + +template +struct AllConvertible { + enum { value = IsConvertible::value && AllConvertible::value }; +}; + +template <> +struct AllConvertible<> { + enum { value = true }; +}; + +static_assert(AllConvertible::value, ""); +static_assert(AllConvertible::value, ""); +static_assert(AllConvertible::value, ""); +static_assert(AllConvertible::value, ""); +static_assert(!AllConvertible>::value, ""); + +template +typename std::enable_if< + AllConvertible::value && sizeof...(OutputTypes) >= 1, + bool>::type +split(const Delim& delimiter, StringPiece input, OutputTypes&... outputs); /* * Join list of tokens. diff --git a/folly/test/ConvTest.cpp b/folly/test/ConvTest.cpp index 92562080..790742bc 100644 --- a/folly/test/ConvTest.cpp +++ b/folly/test/ConvTest.cpp @@ -699,16 +699,13 @@ TEST(Conv, UnsignedEnumClass) { auto u = to(E::x); EXPECT_GT(u, 0); EXPECT_EQ(u, 3000000000U); - auto s = to(E::x); - EXPECT_EQ("3000000000", s); - auto e = to(3000000000U); - EXPECT_EQ(e, E::x); - try { - auto i = to(E::x); - LOG(ERROR) << "to returned " << i << " instead of throwing"; - EXPECT_TRUE(false); - } catch (std::range_error& e) { - } + EXPECT_EQ("3000000000", to(E::x)); + EXPECT_EQ(E::x, to(3000000000U)); + EXPECT_EQ(E::x, to("3000000000")); + E e; + parseTo("3000000000", e); + EXPECT_EQ(E::x, e); + EXPECT_THROW(to(E::x), std::range_error); } // Multi-argument to uses toAppend, a different code path than @@ -848,3 +845,41 @@ TEST(Conv, allocate_size) { toAppendDelimFit(",", str1, str2, &res3); EXPECT_EQ(res3, str1 + "," + str2); } + +namespace my { +struct Dimensions { + int w, h; + std::tuple tuple_view() const { + return tie(w, h); + } + bool operator==(const Dimensions& other) const { + return this->tuple_view() == other.tuple_view(); + } +}; + +void parseTo(folly::StringPiece in, Dimensions& out) { + out.w = folly::to(&in); + in.removePrefix("x"); + out.h = folly::to(&in); +} + +template +void toAppend(const Dimensions& in, String* result) { + folly::toAppend(in.w, 'x', in.h, result); +} + +size_t estimateSpaceNeeded(const Dimensions&in) { + return 2000 + folly::estimateSpaceNeeded(in.w) + + folly::estimateSpaceNeeded(in.h); +} +} + +TEST(Conv, custom_kkproviders) { + my::Dimensions expected{7, 8}; + EXPECT_EQ(expected, folly::to("7x8")); + auto str = folly::to(expected); + EXPECT_EQ("7x8", str); + // make sure above implementation of estimateSpaceNeeded() is used. + EXPECT_GT(str.capacity(), 2000); + EXPECT_LT(str.capacity(), 2500); +} diff --git a/folly/test/StringTest.cpp b/folly/test/StringTest.cpp index 55ceefa0..d453ffb2 100644 --- a/folly/test/StringTest.cpp +++ b/folly/test/StringTest.cpp @@ -906,8 +906,36 @@ TEST(Split, fixed_convert) { EXPECT_EQ(13, b); EXPECT_EQ("14.7:b", d); - EXPECT_THROW(folly::split(':', "a:13:14.7:b", a, b, c), - std::range_error); + + // Enable verifying that a line only contains one field + EXPECT_TRUE(folly::split(' ', "hello", a)); + EXPECT_FALSE(folly::split(' ', "hello world", a)); +} + +namespace my { + +enum class Color { + Red, + Blue, +}; + +void parseTo(folly::StringPiece in, Color& out) { + if (in == "R") { + out = Color::Red; + } else if (in == "B") { + out = Color::Blue; + } else { + throw runtime_error(""); + } +} +} + +TEST(Split, fixed_convert_custom) { + my::Color c1, c2; + + EXPECT_TRUE(folly::split(',', "R,B", c1, c2)); + EXPECT_EQ(c1, my::Color::Red); + EXPECT_EQ(c2, my::Color::Blue); } TEST(String, join) { -- 2.34.1