From 5631e52ed71b4fe01e0cb9146b6ad10ef216b8b0 Mon Sep 17 00:00:00 2001 From: Andy Soffer Date: Fri, 14 Oct 2022 11:18:49 -0700 Subject: [PATCH] Support stringification of user-defined types in AbslStringify in absl::Substitute. We are also moving some internals into an internal header. `HasAbslStringify` was not previously in an internal namespace but was intended to be and has now been moved to an internal namespace. This is in adherence to our compatibility guidelines which wave requirements for APIs within their first 30 days of public release (See https://abseil.io/about/compatibility for details). PiperOrigin-RevId: 481190705 Change-Id: I4c0c348f269ea8d76ea3d4bd5a2c41cce475dc04 --- CMake/AbseilDll.cmake | 1 + absl/strings/BUILD.bazel | 1 + absl/strings/CMakeLists.txt | 1 + absl/strings/internal/stringify_sink.h | 68 ++++++++++++++++ absl/strings/str_cat.h | 103 ++++++++----------------- absl/strings/substitute.h | 31 +++++--- absl/strings/substitute_test.cc | 23 +++++- 7 files changed, 146 insertions(+), 82 deletions(-) create mode 100644 absl/strings/internal/stringify_sink.h diff --git a/CMake/AbseilDll.cmake b/CMake/AbseilDll.cmake index d8ddcb3b..f18b54ad 100644 --- a/CMake/AbseilDll.cmake +++ b/CMake/AbseilDll.cmake @@ -240,6 +240,7 @@ set(ABSL_INTERNAL_DLL_FILES "strings/internal/cordz_update_tracker.h" "strings/internal/stl_type_traits.h" "strings/internal/string_constant.h" + "strings/internal/stringify_sink.h" "strings/match.cc" "strings/match.h" "strings/numbers.cc" diff --git a/absl/strings/BUILD.bazel b/absl/strings/BUILD.bazel index 5b12c010..c6989816 100644 --- a/absl/strings/BUILD.bazel +++ b/absl/strings/BUILD.bazel @@ -42,6 +42,7 @@ cc_library( "internal/stl_type_traits.h", "internal/str_join_internal.h", "internal/str_split_internal.h", + "internal/stringify_sink.h", "match.cc", "numbers.cc", "str_cat.cc", diff --git a/absl/strings/CMakeLists.txt b/absl/strings/CMakeLists.txt index 01f86184..fe82e1df 100644 --- a/absl/strings/CMakeLists.txt +++ b/absl/strings/CMakeLists.txt @@ -41,6 +41,7 @@ absl_cc_library( "internal/charconv_parse.h" "internal/memutil.cc" "internal/memutil.h" + "internal/stringify_sink.h" "internal/stl_type_traits.h" "internal/str_join_internal.h" "internal/str_split_internal.h" diff --git a/absl/strings/internal/stringify_sink.h b/absl/strings/internal/stringify_sink.h new file mode 100644 index 00000000..a83f70e4 --- /dev/null +++ b/absl/strings/internal/stringify_sink.h @@ -0,0 +1,68 @@ +// Copyright 2022 The Abseil Authors +// +// Licensed 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 +// +// https://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. + +#ifndef ABSL_STRINGS_INTERNAL_STRINGIFY_SINK_H_ +#define ABSL_STRINGS_INTERNAL_STRINGIFY_SINK_H_ + +#include +#include +#include + +#include "absl/strings/string_view.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN + +namespace strings_internal { +class StringifySink { + public: + void Append(size_t count, char ch); + + void Append(string_view v); + + bool PutPaddedString(string_view v, int width, int precision, bool left); + + // Support `absl::Format(&sink, format, args...)`. + friend void AbslFormatFlush(StringifySink* sink, absl::string_view v) { + sink->Append(v); + } + + private: + template + friend string_view ExtractStringification(StringifySink& sink, const T& v); + + std::string buffer_; +}; + +template +string_view ExtractStringification(StringifySink& sink, const T& v) { + AbslStringify(sink, v); + return sink.buffer_; +} + +template +struct HasAbslStringify : std::false_type {}; + +template +struct HasAbslStringify(), + std::declval()))>::value>> + : std::true_type {}; + +} // namespace strings_internal + +ABSL_NAMESPACE_END +} // namespace absl + +#endif // ABSL_STRINGS_INTERNAL_STRINGIFY_SINK_H_ diff --git a/absl/strings/str_cat.h b/absl/strings/str_cat.h index 8a63be0d..1a37faae 100644 --- a/absl/strings/str_cat.h +++ b/absl/strings/str_cat.h @@ -48,53 +48,22 @@ // `StrCat()` or `StrAppend()`. You may specify a minimum hex field width using // a `PadSpec` enum. // -// ----------------------------------------------------------------------------- - -#ifndef ABSL_STRINGS_STR_CAT_H_ -#define ABSL_STRINGS_STR_CAT_H_ - -#include -#include -#include -#include -#include -#include - -#include "absl/base/port.h" -#include "absl/strings/numbers.h" -#include "absl/strings/string_view.h" - -namespace absl { -ABSL_NAMESPACE_BEGIN - -namespace strings_internal { -// AlphaNumBuffer allows a way to pass a string to StrCat without having to do -// memory allocation. It is simply a pair of a fixed-size character array, and -// a size. Please don't use outside of absl, yet. -template -struct AlphaNumBuffer { - std::array data; - size_t size; -}; - -//------------------------------------------------------------------------------ -// StrCat Extension -//------------------------------------------------------------------------------ -// -// AbslStringify() +// User-defined types can be formatted with the `AbslStringify()` customization +// point. The API relies on detecting an overload in the user-defined type's +// namespace of a free (non-member) `AbslStringify()` function as a definition +// (typically declared as a friend and implemented in-line. +// with the following signature: // -// A simple customization API for formatting user-defined types using -// absl::StrCat(). The API relies on detecting an overload in the -// user-defined type's namespace of a free (non-member) `AbslStringify()` -// function as a friend definition with the following signature: +// class MyClass { ... }; // // template -// void AbslStringify(Sink& sink, const X& value); +// void AbslStringify(Sink& sink, const MyClass& value); // // An `AbslStringify()` overload for a type should only be declared in the same // file and namespace as said type. // -// Note that AbslStringify() also supports use with absl::StrFormat(). +// Note that `AbslStringify()` also supports use with `absl::StrFormat()` and +// `absl::Substitute()`. // // Example: // @@ -113,33 +82,36 @@ struct AlphaNumBuffer { // int x; // int y; // }; +// ----------------------------------------------------------------------------- -class StringifySink { - public: - void Append(size_t count, char ch); - - void Append(string_view v); +#ifndef ABSL_STRINGS_STR_CAT_H_ +#define ABSL_STRINGS_STR_CAT_H_ - bool PutPaddedString(string_view v, int width, int precision, bool left); +#include +#include +#include +#include +#include +#include - // Support `absl::Format(&sink, format, args...)`. - friend void AbslFormatFlush(StringifySink* sink, absl::string_view v) { - sink->Append(v); - } +#include "absl/base/port.h" +#include "absl/strings/internal/stringify_sink.h" +#include "absl/strings/numbers.h" +#include "absl/strings/string_view.h" - template - friend string_view ExtractStringification(StringifySink& sink, const T& v); +namespace absl { +ABSL_NAMESPACE_BEGIN - private: - std::string buffer_; +namespace strings_internal { +// AlphaNumBuffer allows a way to pass a string to StrCat without having to do +// memory allocation. It is simply a pair of a fixed-size character array, and +// a size. Please don't use outside of absl, yet. +template +struct AlphaNumBuffer { + std::array data; + size_t size; }; -template -string_view ExtractStringification(StringifySink& sink, const T& v) { - AbslStringify(sink, v); - return sink.buffer_; -} - } // namespace strings_internal // Enum that specifies the number of significant digits to return in a `Hex` or @@ -272,15 +244,6 @@ struct Dec { // `StrAppend()`, providing efficient conversion of numeric, boolean, and // hexadecimal values (through the `Hex` type) into strings. -template -struct HasAbslStringify : std::false_type {}; - -template -struct HasAbslStringify(), - std::declval()))>::value>> - : std::true_type {}; - class AlphaNum { public: // No bool ctor -- bools convert to an integral type. @@ -329,7 +292,7 @@ class AlphaNum { AlphaNum(absl::string_view pc) : piece_(pc) {} // NOLINT(runtime/explicit) template ::value>::type> + strings_internal::HasAbslStringify::value>::type> AlphaNum( // NOLINT(runtime/explicit) const T& v, // NOLINT(runtime/explicit) strings_internal::StringifySink&& sink = {}) // NOLINT(runtime/explicit) diff --git a/absl/strings/substitute.h b/absl/strings/substitute.h index 692fd03c..5c3f6eff 100644 --- a/absl/strings/substitute.h +++ b/absl/strings/substitute.h @@ -55,6 +55,8 @@ // * bool (Printed as "true" or "false") // * pointer types other than char* (Printed as "0x", // except that null is printed as "NULL") +// * user-defined types via the `AbslStringify()` customization point. See the +// documentation for `absl::StrCat` for an explanation on how to use this. // // If an invalid format string is provided, Substitute returns an empty string // and SubstituteAndAppend does not change the provided output string. @@ -79,6 +81,7 @@ #include "absl/base/port.h" #include "absl/strings/ascii.h" #include "absl/strings/escaping.h" +#include "absl/strings/internal/stringify_sink.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" @@ -102,14 +105,14 @@ class Arg { // Overloads for string-y things // // Explicitly overload `const char*` so the compiler doesn't cast to `bool`. - Arg(const char* value) // NOLINT(runtime/explicit) + Arg(const char* value) // NOLINT(google-explicit-constructor) : piece_(absl::NullSafeStringView(value)) {} template Arg( // NOLINT const std::basic_string, Allocator>& value) noexcept : piece_(value) {} - Arg(absl::string_view value) // NOLINT(runtime/explicit) + Arg(absl::string_view value) // NOLINT(google-explicit-constructor) : piece_(value) {} // Overloads for primitives @@ -119,7 +122,7 @@ class Arg { // probably using them as 8-bit integers and would probably prefer an integer // representation. However, we can't really know, so we make the caller decide // what to do. - Arg(char value) // NOLINT(runtime/explicit) + Arg(char value) // NOLINT(google-explicit-constructor) : piece_(scratch_, 1) { scratch_[0] = value; } @@ -133,12 +136,12 @@ class Arg { static_cast( numbers_internal::FastIntToBuffer(value, scratch_) - scratch_)) {} - Arg(int value) // NOLINT(runtime/explicit) + Arg(int value) // NOLINT(google-explicit-constructor) : piece_(scratch_, static_cast( numbers_internal::FastIntToBuffer(value, scratch_) - scratch_)) {} - Arg(unsigned int value) // NOLINT(runtime/explicit) + Arg(unsigned int value) // NOLINT(google-explicit-constructor) : piece_(scratch_, static_cast( numbers_internal::FastIntToBuffer(value, scratch_) - @@ -163,17 +166,23 @@ class Arg { static_cast( numbers_internal::FastIntToBuffer(value, scratch_) - scratch_)) {} - Arg(float value) // NOLINT(runtime/explicit) + Arg(float value) // NOLINT(google-explicit-constructor) : piece_(scratch_, numbers_internal::SixDigitsToBuffer(value, scratch_)) { } - Arg(double value) // NOLINT(runtime/explicit) + Arg(double value) // NOLINT(google-explicit-constructor) : piece_(scratch_, numbers_internal::SixDigitsToBuffer(value, scratch_)) { } - Arg(bool value) // NOLINT(runtime/explicit) + Arg(bool value) // NOLINT(google-explicit-constructor) : piece_(value ? "true" : "false") {} - Arg(Hex hex); // NOLINT(runtime/explicit) - Arg(Dec dec); // NOLINT(runtime/explicit) + template ::value>::type> + Arg( // NOLINT(google-explicit-constructor) + const T& v, strings_internal::StringifySink&& sink = {}) + : piece_(strings_internal::ExtractStringification(sink, v)) {} + + Arg(Hex hex); // NOLINT(google-explicit-constructor) + Arg(Dec dec); // NOLINT(google-explicit-constructor) // vector::reference and const_reference require special help to convert // to `Arg` because it requires two user defined conversions. @@ -188,7 +197,7 @@ class Arg { // `void*` values, with the exception of `char*`, are printed as // "0x". However, in the case of `nullptr`, "NULL" is printed. - Arg(const void* value); // NOLINT(runtime/explicit) + Arg(const void* value); // NOLINT(google-explicit-constructor) // Normal enums are already handled by the integer formatters. // This overload matches only scoped enums. diff --git a/absl/strings/substitute_test.cc b/absl/strings/substitute_test.cc index 9e6b9403..9f04545f 100644 --- a/absl/strings/substitute_test.cc +++ b/absl/strings/substitute_test.cc @@ -22,6 +22,16 @@ namespace { +struct MyStruct { + template + friend void AbslStringify(Sink& sink, const MyStruct& s) { + sink.Append("MyStruct{.value = "); + sink.Append(absl::StrCat(s.value)); + sink.Append("}"); + } + int value; +}; + TEST(SubstituteTest, Substitute) { // Basic. EXPECT_EQ("Hello, world!", absl::Substitute("$0, $1!", "Hello", "world")); @@ -70,7 +80,7 @@ TEST(SubstituteTest, Substitute) { // Volatile Pointer. // Like C++ streamed I/O, such pointers implicitly become bool volatile int vol = 237; - volatile int *volatile volptr = &vol; + volatile int* volatile volptr = &vol; str = absl::Substitute("$0", volptr); EXPECT_EQ("true", str); @@ -128,6 +138,11 @@ TEST(SubstituteTest, Substitute) { const char* null_cstring = nullptr; EXPECT_EQ("Text: ''", absl::Substitute("Text: '$0'", null_cstring)); + + MyStruct s1 = MyStruct{17}; + MyStruct s2 = MyStruct{1043}; + EXPECT_EQ("MyStruct{.value = 17}, MyStruct{.value = 1043}", + absl::Substitute("$0, $1", s1, s2)); } TEST(SubstituteTest, SubstituteAndAppend) { @@ -171,6 +186,12 @@ TEST(SubstituteTest, SubstituteAndAppend) { absl::SubstituteAndAppend(&str, "$0 $1 $2 $3 $4 $5 $6 $7 $8 $9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j"); EXPECT_EQ("a b c d e f g h i j", str); + + str.clear(); + MyStruct s1 = MyStruct{17}; + MyStruct s2 = MyStruct{1043}; + absl::SubstituteAndAppend(&str, "$0, $1", s1, s2); + EXPECT_EQ("MyStruct{.value = 17}, MyStruct{.value = 1043}", str); } TEST(SubstituteTest, VectorBoolRef) {