Skip to content

[BUG] No type safety when comparing two incompatible variables #817

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alexgb0 opened this issue Nov 13, 2023 · 10 comments
Closed

[BUG] No type safety when comparing two incompatible variables #817

alexgb0 opened this issue Nov 13, 2023 · 10 comments
Labels
bug Something isn't working

Comments

@alexgb0
Copy link

alexgb0 commented Nov 13, 2023

Describe the bug
No type safety when comparing two variables

To Reproduce
Steps to reproduce the behavior:

  1. Sample code - distilled down to minimal essentials please
main: () -> int = {
	number: int = 123;
	string: std::string = "test";

	std::cout << number > string;
}
  1. Command lines, including which C++ compiler you are using
cppfront -p main.cpp2 &&
g++ main.cpp -std=c++23 -I/root/cppfront/include/ -o main
  1. Expected result - what you expected to happen
    Some kind of error message saying you can't compare incompatible types
  2. Actual result/error
main.cpp:22:19:   required from here
/root/cppfront/include/cpp2util.h:1808:28: error: no match for ‘operator>’ (operand types are ‘std::basic_ostream<char>’ and ‘std::__cxx11::basic_string<char>’)
 1808 |     return CPP2_FORWARD(t) > CPP2_FORWARD(u);
          |                                              ^

Additional context
I know it is a stupid problem, but I didn't pay attention and I have to admit that it took me some time to realize where was the problem.

@alexgb0 alexgb0 added the bug Something isn't working label Nov 13, 2023
@JohelEGP
Copy link
Contributor

@MaxSagebaum Is this what you meant with #810 (comment)?

@JohelEGP

This comment was marked as outdated.

@JohelEGP
Copy link
Contributor

Sorry, I was wrong.
If I remove the parentheses that is added when lowering to Cpp1,
Clang's outputs makes it clear: https://cpp1.godbolt.org/z/f3s9caE57.

<source>:8:22: warning: overloaded operator << has higher precedence than comparison operator [-Woverloaded-shift-op-parentheses]
    8 |         std::cout << number > string;
      |         ~~~~~~~~~~~~~~~~~~~ ^ ~~~~~~
<source>:8:12: note: place parentheses around the '<<' expression to silence this warning
    8 |         std::cout << number > string;
      |                   ^
      |         (                  )
<source>:8:22: note: place parentheses around comparison expression to evaluate it first
    8 |         std::cout << number > string;
      |                             ^       
      |                      (              )
<source>:8:22: error: invalid operands to binary expression ('ostream' and 'std::string' (aka 'basic_string<char>'))
    8 |         std::cout << number > string;
      |         ~~~~~~~~~~~~~~~~~~~ ^ ~~~~~~

@JohelEGP
Copy link
Contributor

So this is a related discussion: #623.

@hsutter
Copy link
Owner

hsutter commented Nov 13, 2023

Yes, I agree this is annoying.

C and C++ << and >> rightly have high precedence because of their built-in meaning as shift operators, where you want the high precedence. But then C++ allowed operator overloading, and particularly in the 1990s (and to a lesser extent today) there was a fashion of overusing of operator overloading. We now know that operator overloading works best when the overloaded operators closely follow the built-in meanings, which avoids the problem that when you do overload them you of course can't change their precedence. We now know that a poster child "bad" example was overloading << and >> for stream I/O, because for that use the operators would want low precedence. Yet it became popular, and it continues to cause problems like this.

For Cpp2, I briefly considered changing << and >> to be lower precedence because their overwhelmingly common use is for streaming... but that would break code that uses them for their original built-in purpose, and be a compatibility issue.

For Cpp2, and all C++ generally including both Cpp1 and Cpp2, I think the best solution is to encourage more use of C++20 std::format and Cpp2 string interpolation, which prevent << chains and naturally put the comparison into a function argument grammar where the precedence is clear:

main: () -> int = {
    n: int = 123;
    s: std::string = "test";

    //  1. Classic << iostream << chaining << style -- needs ( ) around the comparison
    //std::cout << "n>s evaluates to " << n>s << "\n";

    //  2. C++20 format style (will be even nicer in C++26)
    //std::cout << std::vformat("n>s evaluates to {}\n", n>s);

    std::cout << "n>s evaluates to (n>s)$\n";
}

This gives better diagnostics, by avoiding the << precedence issue and leaving only the > type mismatch issue.

Now all the compilers do a lot better -- all compilers still spit a LOT of diagnostics for this because they list all the possible std:: comparison operators they considered but that failed to match, and std:: has a LOT of overloaded comparison operators. But their first message tells you what's going on:

// First diagnostic from Clang
cppfront/include/cpp2util.h:1808:28: error: invalid operands to binary expression ('int' and 'std::basic_string<char>')
    return CPP2_FORWARD(t) > CPP2_FORWARD(u);
           ~~~~~~~~~~~~~~~ ^ ~~~~~~~~~~~~~~~

// First diagnostic from GCC
cppfront/include/cpp2util.h:1808:28: error: no match for ‘operator>’ (operand types are ‘int’ and ‘std::__cxx11::basic_string<char>’)
 1808 |     return CPP2_FORWARD(t) > CPP2_FORWARD(u);
      |                            ^

// First diagnostic from MSVC (because it's considering reversed operators, but still helpful I think)
cppfront\include\cpp2util.h(1808): error C2678: binary '>': no operator found which takes a left-hand operand of type '_T1' (or there is no acceptable conversion)
        with
        [
            _T1=std::basic_string<char,std::char_traits<char>,std::allocator<char>>
        ]

I do wish it was easier to find the original .cpp2 source attribution though...

@hsutter
Copy link
Owner

hsutter commented Nov 13, 2023

OK, I've found a solution that makes the original error message a lot better... I can stick a requires requires on the cpp2::cmp_* comparison functions to require that the requested comparison is valid. That will handle a lot of common cases, including the original example code at top, with a single readable diagnostic.

Now the code at top gets:

  • a nicer error message
  • and in most cases(*) just one! (not a spew of messages)
  • that points back to the original .cpp2 line

The original code at top was:

main: () -> int = {
    number: int = 123;
    string: std::string = "test";

    std::cout << number > string;
}

And with the commit I just pushed, this now gets these error messages:

//== Clang (entire output) ======================================================

In file included from demo.cpp:7:
/mnt/c/GitHub/cppfront/include/cpp2util.h:1843:5: error: static_assert failed due
 to requirement 'program_violates_type_safety_guarantee<std::basic_ostream<char, 
 std::char_traits<char>> &, std::basic_string<char, std::char_traits<char>, std::
 allocator<char>> &&>' "attempted to compare '>' for incompatible types"
    static_assert(
    ^
demo.cpp:22:11: note: in instantiation of function template specialization 'cpp2:
:cmp_greater<std::basic_ostream<char> &, std::basic_string<char>>' requested here
    cpp2::cmp_greater(std::cout << std::move(number),std::move(string));
          ^


//== GCC (entire output) ========================================================

In file included from demo.cpp:7:
/mnt/c/GitHub/cppfront/include/cpp2util.h: In instantiation of ‘constexpr decltyp
e(auto) cpp2::cmp_greater(auto:81&&, auto:82&&) [with auto:81 = std::basic_ostrea
m<char>&; auto:82 = std::__cxx11::basic_string<char>]’:
demo.cpp:22:71:   required from here
/mnt/c/GitHub/cppfront/include/cpp2util.h:1844:9: error: static assertion failed:
 attempted to compare '>' for incompatible types
 1844 |         program_violates_type_safety_guarantee<decltype(t), decltype(u)>,
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


//== MSVC (entire output) =======================================================

cppfront\include\cpp2util.h(1844): error C2338: static_assert failed: 'attempted 
to compare '>' for incompatible types'
demo.cpp(22): note: see reference to function template instantiation 'decltype(au
to) cpp2::cmp_greater<std::basic_ostream<char,std::char_traits<char>>&,std::basic
_string<char,std::char_traits<char>,std::allocator<char>>>(_T0,_T1 &&)' being com
piled
        with
        [
            _T0=std::basic_ostream<char,std::char_traits<char>> &,
            _T1=std::basic_string<char,std::char_traits<char>,std::allocator<char>>
        ]

Much better. Thanks @alexgb0 for pointing this out, and @JohelEGP for the discussion.

(*) There are other examples, such as std::cout << n>s << "\n";, that will still have the usual C++ error message spew mentioned above listing all the std:: comparisons that didn't work (because s << "\n" isn't a valid thing to do to a std::string object s).

@MaxSagebaum
Copy link
Contributor

@MaxSagebaum Is this what you meant with #810 (comment)?

Kind of. But the list of the possible operators is a cpp1 problem. The solution of Herb the way to go I think.

@JohelEGP
Copy link
Contributor

JohelEGP commented Dec 5, 2023

The solution with commit 6b48a72 adds more uses of nonesuch.
These are going to come back biting often when Cpp2 has requires expressions.
That's because a < b will always be well-formed regardless of a and b.

zaucy pushed a commit to zaucy/cppfront that referenced this issue Dec 5, 2023
@hsutter
Copy link
Owner

hsutter commented Dec 14, 2023

The solution with commit 6b48a72 adds more uses of nonesuch.
These are going to come back biting often when Cpp2 has requires expressions.
That's because a < b will always be well-formed regardless of a and b.

Can you give an example that shows the problem, that we can track with a new issue?

@JohelEGP
Copy link
Contributor

JohelEGP commented Jan 5, 2024

For starters (https://cpp2.godbolt.org/z/7r14PWh43):

f: <T, U> (_: T, _: U)
  requires T() < U()
= { }
main: () = {
  // Hard `error: atomic constraint must be of type 'bool' (found 'const nonesuch_')` 👎.
  f(std::integral_constant<int, 4>(),
    std::array<int, 2>());

  // SFINAE-rejected 👍.
  // f(std::integral_constant<int, 4>(),
  //   std::integral_constant<int, 2>());
}

That might not work so well, so here's another example (https://cpp2.godbolt.org/z/zfsjYfobe):

#define REQUIRES_EXPRESSION(X) requires { X }
#define SIMPLE_REQUIREMENT(X) X;
f: <T, U> (_: T, _: U)
  requires REQUIRES_EXPRESSION(SIMPLE_REQUIREMENT(T() < U()))
= { }
main: () = {
  // Static assertion instead of SFINAE-rejection 👎.
  f(std::integral_constant<int, 4>(),
    std::array<int, 2>());

  // Accepted 👍.
  f(std::integral_constant<int, 4>(),
    std::integral_constant<int, 2>());
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants