1

The following code is a simulation of an error I have in a larger code base. The idea is to collect various methods of calculating hashes of objects under one umbrella. The c++20 requires expression together with c++17 constexpr if provide a way to dispatch different strategies based on the input type.

It is entirely valid c++20 code to uses requires expression without concepts and outside a constraint on a template. A requires expression should just evaluate to a constexpr bool and is thus ok to use inside constexpr if.

The various operations are mocked out so as not to add unnecessary dependencies when demonstrating the issue.

https://godbolt.org/z/o79rTKfY8

#include <iostream>

struct A {
    int a ;
    std::string GetHashCode() const {
        return "BHASH";
    }
};

struct B {
    int b ;
    template <typename Archive>
    void serialize(Archive & ar, int ) const
    {
       // No op 
    }
};

struct Archive {
    std::string GetKey(){
        return "GetArchiveKey";
    }
};

template <typename T>
void operator & (Archive & ar, T & t){
    t.serialize(ar, 0);
}

template <typename T>
std::string foo(T const & t)
{
    if constexpr ( requires(Archive ar) { ar & t; }){
        Archive ar;
        ar & t;
        return ar.GetKey();
    }else if constexpr( requires { t.GetHashCode(); }){
        return t.GetHashCode();
    }else
        return "NO HASH";
}


int main() {
    std::cout << foo(A{}) << std::endl ;
    std::cout << foo(B{}) << std::endl ;
}

The error is


<source>:27:7: error: no member named 'serialize' in 'A'
    t.serialize(ar, 0);
    ~ ^
<source>:35:12: note: in instantiation of function template specialization 'operator&<const A>' requested here
        ar & t;
           ^
<source>:45:18: note: in instantiation of function template specialization 'foo<A>' requested here
    std::cout << foo(A{}) << std::endl ;
                 ^
1 error generated.

This is strange because the error occurs in the following context.

 if constexpr ( requires(Archive ar) { ar & t; }){
        Archive ar;
        ar & t;
        return ar.GetKey();
 }

where requires(Archive ar) {ar & t;} is passing but when the body of the if clause is compiled ar & t fails to compile. This is because deeper down the type requires a serialize method. I would have thought this would have been caught in the requires clause, removing this block from consideration.

bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
  • 4
    Maybe the if constexpr just checks if calling `ar & t` is possible. Since there is no concept requirement for that function, it works. When executed, it fails because of no serialize method present in that class. Adding a concept to that operator function will make things work as you expect. `template requires requires(Archive ar, T t) { t.serialize(ar, 0); } void operator & (Archive & ar, T & t){ t.serialize(ar, 0); }` – kiner_shah Apr 05 '23 at 10:39
  • @kiner_shah That is an interesting observation and actually does work for the sample I provided. https://godbolt.org/z/3Erdns9dx However the problem is that the ``void operator & (Archive & ar, T & t)`` function is not under my control. It is provided by boost. It still doesn't quite explain why the original code doesn't work. The expression ``ar & t`` should be invalid. – bradgonesurfing Apr 05 '23 at 10:43
  • I think the answer might be the following statement *The expression is an unevaluated operand; only language correctness is checked* from [cppreference](https://en.cppreference.com/w/cpp/language/requires). But I'm not sure what *language correctness* means because shortly after they contradict themselves and say *"the expression a+b is a valid expression that will compile" with *will compile* being the trigger phrase. – bradgonesurfing Apr 05 '23 at 10:49
  • 1
    Language correctness means syntax. They probably wanna say that if the syntax is correct, then it will compile properly. Semantics are not checked. So it seems this statement justifies what I thought. Maybe they should reword the last statement to something like: *the expression a+b is a valid expression that is it is syntactically valid* – kiner_shah Apr 05 '23 at 10:56
  • Although I am not an expert on concepts, neither I am sure about my assumptions about the working of concepts. But hope that what I wrote in my comment, is somewhat helpful :-) – kiner_shah Apr 05 '23 at 11:05
  • 6
    What if the reason the function *can't work* is buried 10 calls down a call stack!? A compiler can't be expected to verify more than the immediate expression `ar & t` for validy. That's why some templates are called "sfinae-friendly" while others are not. – StoryTeller - Unslander Monica Apr 05 '23 at 11:10
  • Actually you can get the compiler to dive deeper during concept checking. If you make the return value of the function auto and then put a requirement on the return type it will descend except that now the failure occurs within the concept itself. https://godbolt.org/z/17qxYo68Y – bradgonesurfing Apr 05 '23 at 13:09
  • So maybe the question should be rephrased. "Is it possible to write a concept check to see if a type is boost::serializable" I suspect the answer is no. – bradgonesurfing Apr 05 '23 at 13:11
  • 2
    *"except that now the failure occurs within the concept itself*" - The fact it must be instantiated doesn't mean you cheated the system and "made it dive deeper". It just means the function is not sfinae-friendly, again. – StoryTeller - Unslander Monica Apr 05 '23 at 13:57
  • I think, you just hit the narrow gap between a) incomplete classes to work with references , b) require()ing classes to be of specified type and c) concrete function templates to be instanciated upon used members. – Synopsis Apr 05 '23 at 14:08
  • @StoryTeller-UnslanderMonica I'm not claiming to have cheated anything. Just your statement claiming that the compiler cannot be expected to dive 10 layers deep when it does exactly that when resolving auto return types. I expect the reasoning for the compiler deep validating template expressions for auto return types but not validating template expressions for requires expressions is more subtle that you describe. – bradgonesurfing Apr 05 '23 at 14:15
  • Not validating templates all the way down is in lieu of not trying to solve the halting problem. `auto` return types are just an irregularity that is responsible for many working group issues in the tracker. – StoryTeller - Unslander Monica Apr 05 '23 at 14:38

1 Answers1

2

ar & t is a valid expression. There is a function which exists and will be called in accord with the rules of C++ syntax.

That's all requires cares about. Checking whether something inside of that function is legitimate is not requires's job.

So if you want to test that, then you need to implement the & operator in a way that it becomes invalid if something in its body would not work. Of course, since that's defined by some external code, you're more or less out of luck.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982