On Friday, 24 July 2015 at 01:09:19 UTC, H. S. Teoh wrote:
I have trouble thinking of a template function that's actually *correct* when its sig constraints doesn't specify what operations are valid on the incoming type. Can you give an example?

If such code is wrong, I'd say the language *should* reject it.

I see two issues here, both of which relate to maintenance. The first one is that if the language were actually able to check that you missed a requirement in your template constraint (like you're suggesting) and then give you an error, that makes it way easier to break valid code. Take code like this, for example

auto foo(T)(T t)
    if(cond1!T && cond2!T)
{
    ...
    auto b = bar(t);
    ...
}

auto bar(T)(T t)
    if(cond2!T)
{
    ...
}

foo calls bar, and it does have all of bar's constraints in its own constraints so that you don't end up with a compilation error when you pass foo something that doesn't work with bar. Now, imagine if bar gets updated, and now its

auto bar(T)(T t)
    if(cond2!T && cond3!T)
{
    ...
}

but foo's constraint isn't updated (e.g. because foo is in a different library or program that depends no bar, so the person who updates bar isn't necessarily the same person who maintains foo). If the compiler then caught the fact that foo didn't check all of bar's constraints and gave an error, that would alert anyone using foo that foo needed to be updated, but it would also mean that foo would no longer compile, when it's quite possible that the argument passed to foo does indeed pass bar's template constraint and will work just fine with foo. So, working code no longer compiles when there's no technical reason why it couldn't continue to work. Presumably, once the maintainer of foo finds out about this, they'll update foo, and the problem will be fixed, but it still means that every time that the template constraint for bar is adjusted at all, every template that uses it risks breaking if the compiler insists that those templates check all of bar's constraints.

So, yes. it does help ensure that users of foo don't end up with error messages inside of foo thanks to foo's template constraint not listing everything that it actually requires, but it also breaks a lot of code when template constraints change when the code itself will often work just fine as-is (particularly since the change to bar that required a change to its template constraint would usually be a change to its implementation and not what it did, since if you changed what it did, everyone that used it would be broken anyway). Code that's actually broken by the change to bar will fail bar's new template constraint even if the compiler doesn't complain about foo (or any other function) not having updated its constraint, and it'll still get caught. The error might not be as nice, since it'll often be in someone else's templated code, but it'll still be an error, and it'll still tell you what's failing. So, with the current state of affairs, only code that's actually broken by a change to bar's template constraint would be broken and not everyone, whereas what you're suggesting would break all code that used bar that didn't happen to also check the same thing that bar was now checking for.

The second issue that I see with your suggestion is basically what Walter is saying the problem is. Even if we assume that we _do_ want to put all of the requirements for foo - direct or indirect - in its template constraint, this causes a maintenance problem. For instance, if foo were updated to call another function

auto foo(T)(T t)
    if(cond1!T && cond2!T && cond3!T && cond4!T)
{
    ...
    auto b = bar(t);
    ...
    auto c = baz(t);
    ...
}

auto bar(T)(T t)
    if(cond2!T && cond3!T)
{
    ...
}

auto baz(T)(T t)
    if(cond1!T && cond4!T)
{
    ...
}

you now have to update foo. Okay. That's not a huge deal, but now you have two functions that you're using within foo whose template constraints need to be duplicated in foo's template constraint. And ever function that _they_ call ends up affecting _their_ template constraints and then foo in turn.

auto foo(T)(T t)
if(cond1!T && cond2!T && cond3!T && cond4!T && cond5!T && cond6!T && cond7!T)
{
    ...
    auto b = bar(t);
    ...
    auto c = baz(t);
    ...
}

auto bar(T)(T t)
    if(cond2!T && cond3!T)
{
    ...
    auto l = lark(t);
    ...
}

auto baz(T)(T t)
    if(cond1!T && cond4!T)
{
    ...
    auto s = stork(t);
    ...
}


auto lark(T)(T t)
    if(cond5!T && cond6!T)
{
    ...
}

auto stork(T)(T)
    if(cond2!T && cond3!T && cond7!T)
{
    auto w = wolf(t);
}

auto wolf(T)(T)
    if(cond7!T)
{
    ...
}

So, foo's template constraint potentially keeps getting nastier and nastier thanks to indirect calls that it's making. Now, often there's going to be a large overlap between these constraints (e.g. because they're all range-based functions using isInputRange, isForwardRange, hasLength, etc.), so maybe foo's constraint doesn't get that nasty. But where you still have a maintenance problem even if that's the case is if a function that's being called indirectly adds something to its template constraint, then everything up the chain has to add it if you want to make sure that foo gets no compilation internally due to it failing to pass a template constraint of something that it's calling. So, if wolf ends up with a slightly more restrictive constraint in the next release, then every templated function on the planet which used it - directly or indirectly - would need to be updated. And much of that code could be maintained by someone other than the person who made the change to wolf, and much of it could be code that they've don't even know exists. So, if we're really trying to put everything that a function requires - directly or indirectly - in its template constraint, we potentially have a huge maintenance problem here once you start having templated functions call other templated functions - especially if any of these functions are part of a library that's distributed to others. But even if it's just your own code base, a slight adjustment to a template constraint could force you to change a _lot_ of the other template constraints in your code.

So, while I definitely agree that it's nicer from the user's standpoint when the template constraint checks everything that the function requires - directly or indirectly - I think that we have a major maintenance issue in the making here if that's what we insist on. Putting all of the sub-constraints in the top-level constraint - especially with multiple levels of templated functions - simply doesn't scale well, even if it's desirable. Maybe some kind of constraint inference would solve the problem. I don't know. But I think that it is a problem, and it's one that we haven't really recognized yet.

At this point, even if we're going to try and have top-level template constraints explicitly contain all of the constraints of the templates that they use - directly or indirectly - I think that we really need to make sure that the error messages from within templated code are as good as we can make them, because there's no way that all template constraints are going to contain all of their sub-constraints as code is changed over time, not unless the constraints are fairly simple and looking for the same stuff.

Fortunately, the error messages are a lot better than they used to be, but if we can improve them sufficiently, then it becomes less critical to make sure that all sub-constraints be in the top-level constraint, and it makes it a lot more palatable when sub-constraints are missed.

But as I said in the first part, I really don't think that detecting missing constraints and giving errors is a good solution. It'll just break more code that way. Rather, what we need is to either find a way to infer the sub-constraints into the top-level constraint and/or to provide really good error messages when errors show up inside templated code, because a constraint didn't check enough.

- Jonathan M Davis

Reply via email to