Skip to content

Add StringView ctor to Matcher<std::string> for lifetime-safe Property() / Field() use#4968

Draft
Cutuy wants to merge 1 commit into
google:mainfrom
Cutuy:string-view-matcher-symmetry
Draft

Add StringView ctor to Matcher<std::string> for lifetime-safe Property() / Field() use#4968
Cutuy wants to merge 1 commit into
google:mainfrom
Cutuy:string-view-matcher-symmetry

Conversation

@Cutuy
Copy link
Copy Markdown

@Cutuy Cutuy commented Apr 29, 2026

Summary

Adds Matcher<std::string>::Matcher(StringView) and Matcher<const std::string&>::Matcher(StringView), mirroring the existing Matcher<StringView>::Matcher(StringView) implementation. The new constructors copy into a std::string via Eq(std::string(s)) so the matcher owns its data.

Motivation

Today, Matcher<StringView> has an explicit constructor that copies into a std::string:

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

…but the symmetric Matcher<std::string> / Matcher<const std::string&> do not. That asymmetric gap silently miscompiles when callers pass a string_view temporary into a matcher whose target type is std::string — most commonly via Property / Field:

EXPECT_CALL(mock, Method(_, Property(&Proto::string_field,
                                     std::format("{},{}", subkey, topic))));

Property builds Matcher<const std::string&> from the value. With no StringView ctor available for that target, the string_view falls through to MatcherCastImpl and ends up wrapped by polymorphic Eq(string_view)EqMatcher<string_view> stores the view by value. The matcher persists until the test fixture is destroyed; the std::format temporary it points to is destroyed at the end of the EXPECT_CALL statement. The matcher dangles.

Allocator reuse in optimized builds reliably exposes this: subsequent std::format calls in the same scope reuse the same heap slot, so multiple stored matchers all end up pointing into the same recycled buffer holding the last-written contents. We hit it in production code where six EXPECT_CALLs with Property(&Proto::sub_key, std::format(...)) collapsed to all matching the same garbled string in -c opt (but passing in fastbuild).

Change

  • New Matcher(StringView) constructor on Matcher<std::string> and Matcher<const std::string&>, guarded by GTEST_INTERNAL_HAS_STRING_VIEW.
  • Implementations in gtest-matchers.cc that delegate to Eq(std::string(s)), exactly mirroring the existing Matcher<StringView>(StringView) impl.

The change is purely additive — there is no new behavior for code that doesn't currently dangle, and the new path makes a previously-undefined-behavior case well-defined.

Tests

  • StringMatcherTest.CanBeImplicitlyConstructedFromStringView — symmetric counterpart of the existing StringViewMatcherTest.CanBeImplicitlyConstructedFromString.
  • StringMatcherTest.StringViewCtorIsLifetimeSafe — regression test that constructs the matcher from a StringView of a std::string buffer, then mutates and destroys the buffer before invoking Matches(). Without this patch the matcher would hold a dangling view; with the patch it holds an owned copy.

Both pass under cmake locally (gmock-matchers-comparisons_test).

Test plan

  • New tests pass
  • Existing StringViewMatcherTest.* and StringMatcherTest.* still pass
  • Full gmock-matchers-comparisons_test builds cleanly

🤖 Generated with Claude Code

…ring&>

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>
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 29, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@Cutuy Cutuy closed this Apr 29, 2026
@Cutuy Cutuy reopened this Apr 29, 2026
@Cutuy
Copy link
Copy Markdown
Author

Cutuy commented Apr 29, 2026

/recheck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant