Compare commits

...

2 Commits

Author SHA1 Message Date
cutuy-agent
1df4051712
Merge 5062aa991f6257d8fdccb1dfeea676bca7f7c971 into b78aa5ee64be299ae50932e7b4d7e5d7b89a72f6 2026-07-25 14:10:31 +05:30
Jason Cui
5062aa991f Add StringView ctor to Matcher<std::string> and Matcher<const std::string&>
Currently, Matcher<StringView>::Matcher(StringView s) and
Matcher<const StringView&>::Matcher(StringView s) exist and explicitly copy
into a std::string so the matcher owns its data:

    Matcher<internal::StringView>::Matcher(internal::StringView s) {
      *this = Eq(std::string(s));
    }

But the symmetric Matcher<const std::string&>::Matcher(StringView) and
Matcher<std::string>::Matcher(StringView) do not exist. That asymmetric gap
silently miscompiles when callers pass a string_view temporary into matchers
whose target type is std::string -- e.g.

    Property(&Proto::string_field, std::format("{},{}", subkey, topic))

Property() builds Matcher<const std::string&> from the value. Since no
StringView constructor exists for Matcher<const std::string&>, the
string_view falls through to MatcherCastImpl, which builds a polymorphic
Eq(string_view). EqMatcher<string_view> stores the view by value, and the
matcher outlives the calling expression -- the underlying buffer (the
std::format temporary) is destroyed and the matcher dangles.

Allocator reuse in optimized builds reliably exposes this: subsequent
std::format calls reuse the same heap slot, and all stored matchers end up
pointing into the same recycled buffer with the last-written contents.

This patch adds the symmetric constructors that mirror the existing
Matcher<StringView>(StringView) impl: copy into a std::string via
Eq(std::string(s)) so the matcher owns its data.

Also adds:
  - StringMatcherTest.CanBeImplicitlyConstructedFromStringView
  - StringMatcherTest.StringViewCtorIsLifetimeSafe (regression test that
    exercises the matcher after the source buffer is destroyed/mutated)

Signed-off-by: Jason Cui <jcui@nuro.ai>
2026-04-28 23:03:41 -07:00
3 changed files with 60 additions and 0 deletions

View File

@ -272,6 +272,40 @@ TEST(StringViewMatcherTest, CanBeImplicitlyConstructedFromStringView) {
EXPECT_TRUE(m2.Matches("cats"));
EXPECT_FALSE(m2.Matches("dogs"));
}
// Tests that a StringView object can be implicitly converted to a
// Matcher<std::string> or Matcher<const std::string&>. This is the symmetric
// counterpart of CanBeImplicitlyConstructedFromString above; without these
// constructors, a StringView falls through to a polymorphic Eq matcher that
// stores the view by value and dangles past the constructing call.
TEST(StringMatcherTest, CanBeImplicitlyConstructedFromStringView) {
Matcher<std::string> m1 = internal::StringView("cats");
EXPECT_TRUE(m1.Matches("cats"));
EXPECT_FALSE(m1.Matches("dogs"));
Matcher<const std::string&> m2 = internal::StringView("cats");
EXPECT_TRUE(m2.Matches("cats"));
EXPECT_FALSE(m2.Matches("dogs"));
}
// Tests that the matcher copies the string data and is safe to use after the
// underlying buffer the StringView was constructed from is destroyed.
TEST(StringMatcherTest, StringViewCtorIsLifetimeSafe) {
Matcher<std::string> m1;
Matcher<const std::string&> m2;
{
std::string buffer = "cats";
m1 = internal::StringView(buffer);
m2 = internal::StringView(buffer);
// Mutate buffer and let it go out of scope to ensure the matcher does not
// alias |buffer|'s storage.
buffer = "dogs";
}
EXPECT_TRUE(m1.Matches("cats"));
EXPECT_FALSE(m1.Matches("dogs"));
EXPECT_TRUE(m2.Matches("cats"));
EXPECT_FALSE(m2.Matches("dogs"));
}
#endif // GTEST_INTERNAL_HAS_STRING_VIEW
// Tests that a std::reference_wrapper<std::string> object can be implicitly

View File

@ -517,6 +517,13 @@ Matcher<const std::string&> : public internal::MatcherBase<const std::string&> {
// Allows the user to write "foo" instead of Eq("foo") sometimes.
Matcher(const char* s); // NOLINT
#if GTEST_INTERNAL_HAS_STRING_VIEW
// Allows the user to pass absl::string_views or std::string_views directly.
// Copies into a std::string so the matcher owns its data and does not alias
// a (possibly temporary) buffer in the caller.
Matcher(internal::StringView s); // NOLINT
#endif
};
template <>
@ -541,6 +548,13 @@ Matcher<std::string> : public internal::MatcherBase<std::string> {
// Allows the user to write "foo" instead of Eq("foo") sometimes.
Matcher(const char* s); // NOLINT
#if GTEST_INTERNAL_HAS_STRING_VIEW
// Allows the user to pass absl::string_views or std::string_views directly.
// Copies into a std::string so the matcher owns its data and does not alias
// a (possibly temporary) buffer in the caller.
Matcher(internal::StringView s); // NOLINT
#endif
};
#if GTEST_INTERNAL_HAS_STRING_VIEW

View File

@ -93,6 +93,18 @@ Matcher<internal::StringView>::Matcher(const char* s) {
Matcher<internal::StringView>::Matcher(internal::StringView s) {
*this = Eq(std::string(s));
}
// Constructs a matcher that matches a const std::string& whose value is
// equal to s. Copies into a std::string so the matcher owns its data.
Matcher<const std::string&>::Matcher(internal::StringView s) {
*this = Eq(std::string(s));
}
// Constructs a matcher that matches a std::string whose value is equal to s.
// Copies into a std::string so the matcher owns its data.
Matcher<std::string>::Matcher(internal::StringView s) {
*this = Eq(std::string(s));
}
#endif // GTEST_INTERNAL_HAS_STRING_VIEW
} // namespace testing