4

In C# or Java, the following does not compile, because I "forgot" the where part in class declaration, that specifies that T is instance of something that defines the add method.

class C<T> {
    T make(T t) {
        return t.add(t);
    }
}

I'd like to get similar compile-time check in C++, if I specify incomplete requires for a template argument.

template <typename T>
requires true
class C {
public:
  T make() {
    return T{};
  }
};

I'd like to get a compile error for the C++ code above, stating that method c.make relies on T being default-constructible, which is however not captured in the requires constraints on T.

Can I get the compiler to check that my set of requires constraints is sufficient to cover everything the class implementation does?

I am using gcc (GCC) 10.0.0 20191207 (experimental) for this.

Jason
  • 36,170
  • 5
  • 26
  • 60
user7610
  • 25,267
  • 15
  • 124
  • 150
  • 1
    I don't understand. Your `requires std::is_default_constructible` will do exactly that if you un-comment the `delete` in `D`. With the code as written, why would you get a compile time error here? – super Dec 14 '19 at 14:03
  • I "forgot" to write the 'requires std::is_default_constructible'. Now compiler is silent, which is not what I want. I want to be shown error in that case. – user7610 Dec 14 '19 at 14:06
  • 2
    @super I guess he wants to just restrict `C::make` method from being used if `T` is not constructible... But that would be not obvious from the question – Sergey Kolesnik Dec 14 '19 at 14:06
  • 1
    @user7610 Error in what case? `D` is default constructible and there is not a single line in your code that is not commented out that does anything to stop that. Where do you want this magic error to come from? Is the compiler supposed to guess that you forgot to write a certain line of code? – super Dec 14 '19 at 14:08
  • Compiler should see 'return T{};' in make and realise that default-constructibility needs to be required. – user7610 Dec 14 '19 at 14:10
  • @user7610 And you will get that error at compile time IF `D` is not default constructible. But if you pass in a type that is default-constructible like you have done in your example. Why would you get an error then? The code is valid. – super Dec 14 '19 at 14:16
  • So what you actually want is for this code to give a compile-time error if you un-comment the `D() = delete;` but without having to actually call the `make` function? – super Dec 14 '19 at 14:21
  • No, I want to get error for the code as it is. Or, after it is modified to get the compiler to check that "for all types that satisfy the `requires` constraints, all the code in the class must be valid". – user7610 Dec 14 '19 at 14:23
  • Do you need to check that `T` is default_constructible and *ALSO* defines `T::add` method? Or do you need to check if `T` is default-constructible only for a class, and check if it defines `T::add` when `C::make` is called with an argument of type `T`? – Sergey Kolesnik Dec 14 '19 at 14:52
  • Java does not have equivalent of "default-constructible" for generic types (because of type erasure), so the Java example had to use some other property of the type. – user7610 Dec 14 '19 at 14:56
  • I want to check that all possible instantiations of the class that pass the `requires` checks will compile. So that the `requires` checks are exhaustive given what the class does. Which is the guarantee I get from C# or Java, in case you are familiar with those languages. – user7610 Dec 14 '19 at 14:57
  • @user7610 Ok, in that case no. That's not how templates work at the moment. The only compile error you will get without an instatiation is if the code is syntactically invalid. There is no reflection what-so-ever in `c++` yet. – super Dec 14 '19 at 16:00
  • Definition checking is unrelated to reflection. And there _is_ actually reflection in `c++`, https://en.wikipedia.org/wiki/Run-time_type_information. – user7610 Dec 14 '19 at 16:06
  • @user7610 Whatever lingo you prefer to use, clearly RTTI has nothing to do with compile-time errors. Even someone not familiar with the language should understand that. Not very hard to argue that if the compiler needs to examine the whole object as an entity and make decision based on that it's using reflection in some sense of the word. Let's call it compile-time reflection then. – super Dec 14 '19 at 17:55

2 Answers2

2

What I want is called definition checking, and apparently it is not possible currently

C++2a concepts (formerly known as “Concepts Lite” and/or the Concepts TS) famously do not support “definition checking.” The idea of definition checking is that the programmer might write [...]

https://quuxplusone.github.io/blog/2019/07/22/definition-checking-with-if-constexpr/

8.2 Definition checking

Concepts currently do not prevent a template from using operations that are not specified in the requirements. Consider:

template<Number N>
  void algo(vector<N>& v){
    for (auto& x : v) x%=2;
  }

Our Number concept does not require %=, so whether a call of algo succeeds will depend not just on what is checked by the concept, but on the actual properties of the argument type: does the argument type have %=? If not, we get a late (instantiation time) error.

Some consider this a serious error. I don’t [...]

http://www.w.stroustrup.com/good_concepts.pdf

The current proposal checks interfaces and that's where the main benefits for users are, but not template definitions. That has been explicit from the start.

https://isocpp.org/blog/2016/02/a-bit-of-background-for-concepts-and-cpp17-bjarne-stroustrup

Community
  • 1
  • 1
user7610
  • 25,267
  • 15
  • 124
  • 150
  • Definition checking is not possible yet withing concepts. But I showed you how to have such a check with SFINAE and show an error. Doesn't it solve your problem? – Sergey Kolesnik Dec 15 '19 at 06:28
  • So your answer is basically not right, because your initial question is about the ability to statically check *something*, not the ability to do it with Concepts. – Sergey Kolesnik Dec 15 '19 at 06:34
  • Yes, it is about statically checking something at declaration time. And that cannot be done in C++. It cannot be done with regular templates (templates are completely checked later, at instantiation time, and only against the specific type used in the instantiation) and it cannot be done even with concepts, which was my best hope. I'd welcome some other way to do the definition checking, but it seems to me that c++ itself cannot do it. Maybe some additional tooling can. – user7610 Dec 15 '19 at 10:53
  • What cannot be done? When exactly do you want to run that kind of check? Before template intsantiation? Well, how exactly does the compiler knows whether it must run check or not? I am not familiar with that other languages and their particular features you expect to implement with concepts or whatever. Can you provide an example scenario (maybe in pseudocode) of what you want to do and what kind of error do you want to get and when do you want to get it? – Sergey Kolesnik Dec 15 '19 at 13:17
  • 1
    See the C++ code in the question for the example code, and the paragraph below the code for the compile error I want to get. Or look at the example in Bjarne Stroustrup's explanation of "definition checking", which I quoted in this answer. I think Bjarne explains this quite well. (It's his job, anyways.) – user7610 Dec 15 '19 at 13:22
  • Ok. What is wrong with the following approach? : 1. Define a template traits struct, which checks (**statically, at compile time** as you state in your question) that an argument `T` is: *a.* default constructible; *b.* has method `T::add`, *c.* whatever else you need. For each check it has a specific *compile-time* error. 2. You use this traits class in your template class definition. 3. If `T` is invalid, `"the following does not compile"©`. – Sergey Kolesnik Dec 15 '19 at 13:30
  • 1
    It does not provide definition checking. I will not get a compile error if the template traits struct is missing a necessary check; necessary given how T is used in the definition of my class. – user7610 Dec 15 '19 at 14:15
  • what kind of definition checking are you talking about??? `Concepts currently do not prevent a template from using operations that are not specified in the requirements.` Of course it will not check if you **didn't ask it to.** And I give you a way to do just that: you can ask, if `T` has a definition for method `add` and gives you a compile time error if not. You can write a trait check for all the methods in the world. But in order to check for them, you need to include that check yourself. Because, `how the compiler will know what you need`© – Sergey Kolesnik Dec 15 '19 at 14:19
  • The compiler can look at the definition. – user7610 Dec 15 '19 at 14:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/204297/discussion-between-sergey-kolesnik-and-user7610). – Sergey Kolesnik Dec 15 '19 at 14:22
  • @Sergey I think you're missing the entire point of definition checking - which is to know that the implementation of my template is correct at the point of definition. – Barry Dec 15 '19 at 14:52
  • @Barry, maybe. In my answer I check if methods of a given argument `T`, that are necessary for the implementation of `C` to work, are defined – Sergey Kolesnik Dec 15 '19 at 15:03
  • 3
    @SergeyKolesnik It's not practical to expect everyone, for _every template_, to write a suite of compile-or-not-compile tests for all interesting types. It's not even really practical to expect people to correctly come up with the interesting types to begin with. – Barry Dec 15 '19 at 15:08
  • @Barry you are saying: `it's not practical to expect everyone to run every necessary check for a code to work properly` lemao. And yes, it is not practical at all. Nevertherless it is a necessary practice – Sergey Kolesnik Dec 15 '19 at 15:11
  • 2
    We have type systems for a reason. It is good to offload work onto type systems, even if they sometimes get in the way... It is asserted that large software cannot be written and maintained without at least some static checking in place. https://blogs.dropbox.com/tech/2019/09/our-journey-to-type-checking-4-million-lines-of-python/ Here's article arguing that definition checking is a necessary practical feature http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0240r0.html#DefinitionChecking – user7610 Dec 15 '19 at 15:15
  • @Sergey It's a necessary practice (... that nobody outside of standard library implementers does, and even they get it wrong regularly) in C++ only because we don't have definition checking. It is not a necessary practice in Rust or Swift or ... – Barry Dec 15 '19 at 16:01
  • Thing is, it does make much sense not to have definition checking be part of the concepts feature: * programmers with background in C++ do not expect to have it available * it would make modernizing old code harder * it would interfere with metaprogramming tricks * it would limit concepts we can declare only to those that can be definition-checked * C++ is popular enough that some checking can be retrofitted later in some tooling, there will be the motivation and the people to do it – user7610 Dec 16 '19 at 09:05
0
template <class T>
requires std::is_default_constructible_v<T>
class C
{
    static_assert(std::is_default_constructible_v<T>, 
        "T is not default-constructible");
};

struct valid
{

};

class invalid
{
    invalid() = delete;
};

int main()
{
    C<valid>();
    // C<invalid>(); // assertion fails.
}

You can write static_assert anywhere inside the class definition, alongside with requires. That will give you an error message you want.


UPDATE After reading the link you have provided, I suppose you just need multiple checks.

You can write a traits struct:


// SFINAE to check if has "add"
template <class T, class = std::void_t<>>
struct has_method_add
{
    constexpr static bool value = false;
};

template <class T>
struct has_method_add<T, std::void_t<decltype(&T::add)>>
{
    constexpr static bool value = true;
};

template <class T, class = std::void_t<>>
struct has_operator_remainder
{
    constexpr static bool value = false;
};

template <class T>
struct has_operator_remainder<T, std::void_t<decltype(&T::operator%=)>>
{
    constexpr static bool value = true;
};

template <class T>
struct error_missing_add
{
    constexpr static bool value = has_method_add<T>::value;
    static_assert(has_method_add<T>::value, "T::add is not defined");
};

template <class T>
struct error_missing_remainder
{
    constexpr static bool value = has_operator_remainder<T>::value;
    static_assert(has_operator_remainder<T>::value, "T::operator%= is not defined");
};

template <class T>
class C
{
    static_assert(std::conjunction_v<error_missing_add<T>, error_missing_remainder<T>>);
    // impl...
};

struct valid
{
    void add();
    int operator%=(int) const;
};

struct missing_add
{
    int operator%=(int) const;
};

struct missing_remainder
{
    void add();
};

int main()
{
    C<valid>{};

    C<missing_add>{}; // error: T::add is not defined
    C<missing_remainder>{}; // error: T::operator%= is not defined

    return 0;
}
user7610
  • 25,267
  • 15
  • 124
  • 150
Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • This is helpful. It will give me compile error when I try to instantiate the template with something that is not default-constructible. I've updated the question to hopefully explain better what I want. – user7610 Dec 14 '19 at 14:38
  • I am getting both static asserts in the compile output, not just the first, as you mentioned in chat. So this seems to work. But it is not definition checking. – user7610 Dec 15 '19 at 15:26
  • It is the best we can hope to do with current compilers. We need to write a suite of compiles/not-compiles tests, essentially, to have at least some assurance we got the `requires` right. I wonder if I can do this with SFINAE and such tricks, or if I need to explicitly invoke the compiler against test files... I am going to upvote this. – user7610 Dec 15 '19 at 15:35
  • 1
    @user7610 there is a special [command](https://cmake.org/cmake/help/latest/command/try_compile.html) for this in cmake. You may define in a cpp file all the checks and add a cmake test. These tests can run automatically all at once – Sergey Kolesnik Dec 15 '19 at 15:38