C++ Logo

std-discussion

Advanced search

Re: Making the new expression smart

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Fri, 18 Sep 2020 16:25:14 -0400
On Fri, Sep 18, 2020 at 2:36 AM Richard Hodges via Std-Discussion
<std-discussion_at_[hidden]> wrote:
> On Thu, 17 Sep 2020 at 23:52, Ville Voutilainen <ville.voutilainen_at_[hidden]> wrote:
>> On Thu, 17 Sep 2020 at 23:51, Richard Hodges via Std-Discussion
>> <std-discussion_at_[hidden]> wrote:
>> Personally I tend to prefer keywords that express intent rather than
>> library functions,
>>
>> Why?
>
>
> Thanks for asking. My position is pretty simple. It’s about elegance, readability and ultimately, performance.
>
> Elegance & readability:
> Library functions are wordy, namespaced and by definition, a compromise when seeking to express intent to both programmer and compiler. This inevitably results in wordiness, preconditions that are left unchecked, the possibility of UB and complicated syntax that Is expressed in and limited by the syntax of templates and function calls. A keyword with a specific job allows expression in terms of precise intent.
>
> E.g Compare (ignoring current syntax transgressions):
>
> auto x = std::make_tuple(foo, bar);
>
> to
>
> auto x = { foo, bar }; // shorter and now a language type
>
> or
>
> auto x = tuple [ foo, &bar]; // bar is a tie
>
> or
>
> auto x = tuple & [ foo, bar ]; // equivalent to std::tie
>
> In the above trivial example, the presence of the name std::make_tuple adds almost no information. It also creates a type with a long and unwieldy name (that actually causes some compilers to run out of string space!) and an eye-wateringly complex implementation. The presence of std:: adds absolutely none as a tuple is a globally understood concept. The last two examples suggest a similar capture model to that of lambdas, which provides consistency.
>
> Performance:
> Expression through library templates means two levels of translation of intent. One from user intent to expression in terms of existing library code and (sparse) core language features. There is plenty of opportunity for missing concepts in the language to limit the quality of that translation.
> On the other hand, a keyword with code generation behind it means that a users’ specific intent can be translated into optimal code every time. Furthermore, as compiler technology improves, that translation can be improved, improving every program upon recompilation.
> Of course this is partially true of libraries, but the library still suffers from not having access to direct code generation with knowledge of the absolute intent of the programmer.

Personally, I find these justifications to be missing the forest from
the trees, though that probably comes from a fairly different set of
core values for language design.

"Elegance" and "readability" are mostly a matter of taste. Some people
hate seeing any namespace in code. That's not really a position that's
useful to argue with. Same goes for seeing template parameters.
Namespaces and templates are core parts of the C++ language, and
expecting that you will only rarely use them is... not reasonable.

Now yes, does C++ sometimes require you to add more information than
is strictly required? Yes. Even CTAD doesn't allow you to avoid having
a `std::pair` wrapped around your sequence of values when initializing
a `std::map` from a braced-init-list. There are times when it's clear
that something could hypothetically be expressed more clearly in the
language.

The issue is that... well, what happens when I have to make my own
`map` type? All of that "elegance" and "readability" I had when using
the standard library type goes away. I have to add more information,
use template arguments, and other things I didn't have to when using
the language map types.

Biasing the convenient API against user-defined types only helps users
to the extent that they're willing to give up control to those
baked-in types.

Where I think you're missing the forest for the trees is the fact that
you're trying to focus on justifications for making *everything* a
language type rather than the specific cases where there are practical
benefits to making them language types.

`std::tuple` is an adequate type. It provides indexed access
functionality and it gets the basic job done. But it could be so much
more, and most of that is not reasonably possible as a C++ library
type.

For example, a `std::tuple<A, B, C>` is logically isomorphic with any
`struct Typename {A a; B b; C c;};`. But there are many differences in
them, and not just on how you access the subobjects. If A, B, and C
are trivially copyable, so too will `Typename` be. But not `tuple<A,
B, C>`. The same goes for standard layout, along with its layout
compatibility rules for unions and the like. The needs of implementing
`tuple` efficiently within the library prevent these things.

Logically, there is no reason why a tuple could not be implicitly
convertible to a decomposible struct containing the same sequence of
types (or a sequence of implicitly-convertible types). But it isn't.
That's not a thing you can express in the language, so that's not a
thing that you can actually implement on the type.

This isn't a matter of whether a compiler can see through something or
not. It's not a matter of compiler optimization; these are things that
ought to be logically possible which are not possible so long as the
tuple is a library type.

> Further:
> Many modern library features rely on compiler intrinsics - from the programmer’s point of view these are required but undocumented language extensions. This is untenable at scale.

Compiler intrinsics are used in standard library implementations in
one of two broad categories: tests for things that are otherwise
impossible for the language at present to determine (ie: most
type-traits), and workarounds for things that the standard requires
implementations to be able to do but the C++ object model doesn't
allow (ie: most of the non-traits intrinsics you see in `std::vector`
implementations).

The former could hypothetically be exposed by reflection mechanisms,
but reflection will not in most cases be as fast as a compiler
intrinsic. I mean sure, you could write a `consteval` function that
takes a `meta` value, tests if it is a type, and then compares it to
all of the various integer types to see if it's one of them. But I'm
pretty sure that's going to be a lot slower to execute at compile time
than a compiler intrinsic hidden behind `std::is_integer`.

As far as the object model issues are concerned, the committee is
already making changes to the object model to make these things
possible. In C++20, you can now implement std::vector (mostly?)
without using compiler intrinsics to avoid UB.

So... what's the problem? Reflection is still being worked on, but
will be slow for doing most of the basic traits intrinsics anyway. And
the object model is being improved to make those object model-based
intrinsics unnecessary.

> Better, in my view, to have no intrinsics and simply extend the language to include these obviously essential features.

"Simply extend the language" is not in any way, shape, or form
"simple". That's the thing you're not quite understanding about why
quite a few of these things are library features: because it's
*simpler* to do it that way. It's way easier to deploy some high-value
type traits than it is to implement a complete reflection system.

Received on 2020-09-18 15:25:27