From c8510941820ddb500af7b23b965f71f3ef6b78cc Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 26 Jul 2026 08:57:53 +0000 Subject: [PATCH] Require exact return-type matches in generated protocol concepts protocol.j2's generated concepts used std::convertible_to, letting merely-convertible return types conform; switched to std::same_as per DRAFT.md's exact-match design. CovariantBImpl no longer conforms to B and moves from a dispatch test to a negative static_assert. --- protocol_test.cc | 28 +++++++++++++--------------- scripts/protocol.j2 | 12 ++---------- scripts/test_concept_errors.py | 2 +- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/protocol_test.cc b/protocol_test.cc index 86eec78..664de2a 100644 --- a/protocol_test.cc +++ b/protocol_test.cc @@ -506,41 +506,39 @@ TEST(ProtocolTest, SwapFromValueless) { EXPECT_TRUE(ppp.valueless_after_move()); } +// A B-shaped type whose methods return covariant/implicitly-convertible +// types instead of B's exact declared return types. class CovariantBImpl { std::string last_input_; public: - // Testing discard: interface returns void, implementation returns int int process(const std::string& input) { last_input_ = input; return 42; } - // Testing implicit conversion: returns an implicitly convertible struct - // instead of std::vector struct VectorConvertible { operator std::vector() const { return {1, 2, 3}; } }; VectorConvertible get_results() const { return {}; } - // Testing implicit conversion: returns const char* instead of bool const char* is_ready() const { return "ready"; } std::string last_input() const { return last_input_; } }; -TEST(ProtocolTest, CovariantReturns) { - xyz::protocol p(std::in_place_type); - - p.process("hello covariant"); - EXPECT_TRUE(p.is_ready()); - - std::vector results = p.get_results(); - ASSERT_EQ(results.size(), 3u); - EXPECT_EQ(results[0], 1); - EXPECT_EQ(results[1], 2); - EXPECT_EQ(results[2], 3); +TEST(ProtocolTest, CovariantReturnsDoNotConform) { + // protocol requires exactly matching function signatures; it does not + // accept conformance through implicit conversion (the relaxed structural + // subtyping DRAFT.md considers and rejects as a design alternative). + // CovariantBImpl's methods are all individually callable but none returns + // B's exact declared type, so it does not conform. + static_assert( + !std::constructible_from, + std::in_place_type_t>, + "protocol should not be constructible from a type whose methods " + "return covariant/convertible, not exact, types"); } TEST(ProtocolViewTest, ViewFromMutableConcrete) { diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index e039de5..d15ac5b 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -32,11 +32,7 @@ concept protocol_const_concept_{{ c.name }} = requires(const T& t) { {% set _ = declvals.append("std::declval<" ~ a.type.name ~ ">()") %} {% endfor %} {% set args_str = declvals | join(", ") %} - {% if m.return_type.name == 'void' %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %}; - {% else %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %} -> std::convertible_to<{{ m.return_type.name }}>; - {% endif %} + { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %} -> std::same_as<{{ m.return_type.name }}>; {% endif %} {% endfor %} }; @@ -52,11 +48,7 @@ concept protocol_concept_{{ c.name }} = protocol_const_concept_{{ c.name }}{% {% set _ = declvals.append("std::declval<" ~ a.type.name ~ ">()") %} {% endfor %} {% set args_str = declvals | join(", ") %} - {% if m.return_type.name == 'void' %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %}; - {% else %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %} -> std::convertible_to<{{ m.return_type.name }}>; - {% endif %} + { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %} -> std::same_as<{{ m.return_type.name }}>; {% endfor %} }{% endif %}; diff --git a/scripts/test_concept_errors.py b/scripts/test_concept_errors.py index 9aa8c2e..b58ad3e 100644 --- a/scripts/test_concept_errors.py +++ b/scripts/test_concept_errors.py @@ -113,7 +113,7 @@ def test_wrong_return_type(compile_check: Callable[[str, List[str]], None]) -> N class BadALike_WrongReturnType { public: std::string_view name() const noexcept { return "name"; } - std::string count() { return "42"; } // not convertible to int + std::string count() { return "42"; } // not the same type as int }; void test() {