3

I want to define a class template Bar<T>, which uses another class template Foo<U>, and the template parameter T is passed as the template argument of Foo<U> somewhere.

Then if U in Foo<U> has some concept constraits (std::floating_point for example), should I repeat the constrait on T when defining Bar?

#include <concepts>

template <std::floating_point U>
struct Foo { /* ... */ };

// <typename T> or <std::floating_point T>?
template <typename T>
struct Bar {
    void Func() {
        Foo<T> foo;  // Uses Foo here
        /* ... */
    }
};

<typename T> and <std::floating_point T>, what are the benefits and tradeoffs of each version?

5
  • 1
    asking for opinions is offtopic. THough, why would you not specify Bar to require U to be std::floating_point ? Commented Apr 17 at 16:39
  • 1
    is Bar<T> usable without instantiating Foo<T> ? Commented Apr 17 at 16:40
  • @463035818_is_not_an_ai Bar<T> must instantiate Foo<U> here in my project, and no other positions in Bar require this concept. Commented Apr 17 at 16:54
  • 2
    yes, indeed asking for opinions is off topic here. However I see real value in this question. I think you should change "which is a better practice" to "what are the benefits and tradeoffs of each version". Commented Apr 18 at 9:07
  • @bolov OK, I've changed it. Commented Apr 18 at 10:20

5 Answers 5

5

Specifying the same constraint in two places is both duplication and a form of coupling.

Therefore you should only do this if it actually fixes or improves something.

You're not using SFINAE and you'll get a reasonable error message when the instantiation of Foo fails anyway, in this case there's no benefit, and some disbenefit. Don't do it.

Sign up to request clarification or add additional context in comments.

5 Comments

But the repeating of trait in Rust and typeclass in Haskell (which is similar to concepts in C++) seems to be compulsory by the compiler (I heard of this, I'm not a Rust or Haskell expert). Are there some difference between the design philosophy of C++ and that of Rust and Haskell?
@TimothyLiu - For sure there is. C++ treats backwards compatibility as sacred (for the most part). Concepts affect overload resolution, and constraining an overload must be done with care to not silently change the meaning of a call to an overloaded function. If concepts were made viral like rust traits, it'd be impossible to introduce them into an existing template code implementation without risking havoc in the public overload set.
@StoryTeller-UnslanderMonica So in C++ we needn't propagate constraints like Rust, right? Or is propagating constraints also a coding style in C++ if there are no compatibility issues?
@TimothyLiu That is true for Rust traits and Haskell typeclasses, but C++ concepts are extremely different things.
@TimothyLiu - I hate to call it "style", because it affects the meaning of code (if not now, then possibly in the future when overloads or template specializations may be added). This is not some purely aesthetic aspect of the code. That is why I find the advice in this answer solid.
5

I would say you have 2 options to be SFINAE friendly,

Either constraint the whole class:

template <std::floating_point U>
struct Foo { /* ... */ };

template <std::floating_point T>
struct Bar {
    void Func() {
        Foo<T> foo;  // Uses Foo here
        /* ... */
    }
};

or, constraint only the method:

template <std::floating_point U>
struct Foo { /* ... */ };

template <typename T>
struct Bar {
    void Func() requires(std::floating_point<T>) {
        Foo<T> foo;  // Uses Foo here
        /* ... */
    }
};

Which one is the most adapted would depend of the purpose of the classes/methods

Comments

4

None of the answers have pointed this out yet, so I'll just add that you could also just refer to the other template's constraints:

template <typename T>
struct Bar {
    void Func() requires requires { typename Foo<T>; }
    {
        Foo<T> foo;  // Uses Foo here
        /* ... */
    }
};

This doesn't require repeating/knowing Foo's constraints, simply that there are any. This way, if Foo<T> changes from requiring floating_point<T> to some other (or some additional) constraint, you pick that up.

Comments

1

Write a one-sentence or one-paragraph overview of the purpose of Bar. On the basis of that overview, is it reasonable to infer a constraint on the template parameter? If so, add that constraint. It might be that Bar has an intrinsic reason for a constraint that you had overlooked (brought to your attention by the use of Foo).

If not, you might want to reconsider if your use of Foo is justified. If it is, you might want to constrain just the function that uses Foo, and constrain it on the basis of Foo<T> being valid (e.g. requires requires { typename Foo<T>; }), since that accurately reflects why (and where) there is a constraint. This also minimizes the "blast radius" should something break because of changes to other parts of the code.


One other consideration I would take into account is user-friendliness. How actionable is the error message should someone provide an invalid template argument? After all, one of the benefits of concepts is greater readability of error messages. Try compiling intentionally-bad code like Bar<int> b; b.Func(); and see what your compiler tells you. Does your compiler give you enough information to fix the error? Or is the message likely to send someone to some Q&A website somewhere to ask for clarification?

For instance, if you do not add a constraint to Bar, the error message might lead someone to think the error is in Bar rather than in their use of Bar. In my trial, the error message did mention the line number where Func() was invoked, but it called out the line Foo<T> foo; // Uses Foo here in Bar::Func() as the likely location of the error. On the surface, that looks like an error in Bar. You might be blamed as the author of Bar.

Also consider whether it would be helpful to have a compilation error even if the programmer forgets to invoke Func(). If every valid use-case of Bar necessitates calling Func(), it might be worth having the constraint on Bar itself rather than on Bar::Func(). (Otherwise, adding a forgotten call to Func() could cause a compilation error, making the fix look more like a break.)

So a lot depends on the purpose of Bar. As a starting point, I would constrain Bar::Func to template parameters for which its definition is valid; i.e. for which Foo<T> is valid. Then I'd take a look at the error messages, factor in the experience level of the programmers, and make a judgment call.

Comments

1

Then if U in Foo<U> has some concept constraits (std::floating_point for example), should I repeat the constrait on T when defining Bar?

You could1 constrain just the function that uses Foo<U>, without an explicit mention of its original constraints, but so that it could actually use that type:

#include <concepts>

template <std::floating_point U>
struct Foo { /* ... */ };

template <typename T>
struct Bar
{
  void Func()
    requires std::constructible_from<Foo<T>>
    //       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  {
    [[maybe_unused]] Foo<T> foo;  
    /* ... */
  }
};

int main()
{
  Bar<float> a;
  a.Func();
  Bar<int> b;
  // ^^^^^  Note that compilers won't complain here
  b.Func();
}

You'll gain a bit of decoupling, but the error will expose the internal usage of a Foo<T>, rather than a generic floating-point type constraint for T.


1) Testable at: https://godbolt.org/z/8naE4b8sv

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.