Date: Sat, 04 Apr 2026 08:31:59 -0700
On Saturday, 4 April 2026 02:43:35 Pacific Daylight Time Simon Schröder via
Std-Proposals wrote:
> Muneem mentioned the programming language Go as an example. Go does not have
> subtyping. Instead, you can define interfaces. Traditionally, heterogeneous
> arrays have been defined as type []interface{} which is an array of an
> empty interface (disclaimer: I have never written a single line of Go). The
> empty interface somehow allows to store any type. The modern version of
> this seems to be to use an array of the ‘any’ type. This would perfectly
> map to std::vector<std::any> in C++. The example that I have seen is that
> it is still possible to at least println() every single entry inside an
> []interface{}. So, there needs to be some sort of runtime polymorphism.
> This again has the aforementioned problem of instantiating all templates
> and linking all functions.
You can do qDebug() on a QVariantList and it will attempt to call qDebug in
each element. But that's because we have extracted the qDebug operator for
types. You couldn't do the same for std::format(), unless you accepted the
debug format output.
So the question for the Go case: can you *only* do println(), or can you do
anything? Could you call an arbitrary member function like "release_cat()"?
And what happens if the type doesn't have such a member function?
There are two problems with this, one of which you've identified. Right now,
the functions in question may not be anywhere in the executable. To implement
this, one must have all possible functions emitted, which is a tall order and
will increase code size. The second is that types must be registered so finding
all those functions is possible.
I don't even think the first one is possible at all. If we restricted to member
functions, maybe it would be. But let's take the example of std::format: the
thing that needs to be "registered" is a specialisation of an unrelated type
(std::formatter) for the type in question. The compiler may not know this
specialisation exists, because it may be declared somewhere else entirely.
Worse, because it's a specialisation, the compiler may have multiple partial
matches.
That's exactly a problem we have right now with the qDebug() on arbitrary
types that I started the discussion with: we try to extract the operator<<,
but it need not be available at all points where the type gets registered with
QMetaType, because it's optional. That leads to ODR violations, which manifest
as the debug output working or not working depending on the order in which
source files were compiled and what portion of the codebase executed first.
> The compiler might actually create a list of function pointers (even now
> with std::visit) that can be indexed based on the type. I don’t know if
> function pointers are better in any way for std::visit than the solution we
> have right now.
Depends of course on what you're comparing to. If the compiler has inlined
almost everything and performed some minimal code deduplication, it's probably
going to be somewhere from "worse" to "much worse". Taking the earlier example
of .capacity() on a vector: if in some implementation it is stored as a number
at exactly at the same relative memory locations, the actual type would be
irrelevant. Will the compiler realise this?
This is not the case in the major implementations: size and capacity are
implemented as pointers to the end instead. So actually returning end() could
be an example: https://gcc.godbolt.org/z/Ejs9a1Thr. GCC/libstdc++ did the full
type-erasing. MSVC did too, but left the "bad variant" case and failed to
eliminate some unnecessary comparisons.
Std-Proposals wrote:
> Muneem mentioned the programming language Go as an example. Go does not have
> subtyping. Instead, you can define interfaces. Traditionally, heterogeneous
> arrays have been defined as type []interface{} which is an array of an
> empty interface (disclaimer: I have never written a single line of Go). The
> empty interface somehow allows to store any type. The modern version of
> this seems to be to use an array of the ‘any’ type. This would perfectly
> map to std::vector<std::any> in C++. The example that I have seen is that
> it is still possible to at least println() every single entry inside an
> []interface{}. So, there needs to be some sort of runtime polymorphism.
> This again has the aforementioned problem of instantiating all templates
> and linking all functions.
You can do qDebug() on a QVariantList and it will attempt to call qDebug in
each element. But that's because we have extracted the qDebug operator for
types. You couldn't do the same for std::format(), unless you accepted the
debug format output.
So the question for the Go case: can you *only* do println(), or can you do
anything? Could you call an arbitrary member function like "release_cat()"?
And what happens if the type doesn't have such a member function?
There are two problems with this, one of which you've identified. Right now,
the functions in question may not be anywhere in the executable. To implement
this, one must have all possible functions emitted, which is a tall order and
will increase code size. The second is that types must be registered so finding
all those functions is possible.
I don't even think the first one is possible at all. If we restricted to member
functions, maybe it would be. But let's take the example of std::format: the
thing that needs to be "registered" is a specialisation of an unrelated type
(std::formatter) for the type in question. The compiler may not know this
specialisation exists, because it may be declared somewhere else entirely.
Worse, because it's a specialisation, the compiler may have multiple partial
matches.
That's exactly a problem we have right now with the qDebug() on arbitrary
types that I started the discussion with: we try to extract the operator<<,
but it need not be available at all points where the type gets registered with
QMetaType, because it's optional. That leads to ODR violations, which manifest
as the debug output working or not working depending on the order in which
source files were compiled and what portion of the codebase executed first.
> The compiler might actually create a list of function pointers (even now
> with std::visit) that can be indexed based on the type. I don’t know if
> function pointers are better in any way for std::visit than the solution we
> have right now.
Depends of course on what you're comparing to. If the compiler has inlined
almost everything and performed some minimal code deduplication, it's probably
going to be somewhere from "worse" to "much worse". Taking the earlier example
of .capacity() on a vector: if in some implementation it is stored as a number
at exactly at the same relative memory locations, the actual type would be
irrelevant. Will the compiler realise this?
This is not the case in the major implementations: size and capacity are
implemented as pointers to the end instead. So actually returning end() could
be an example: https://gcc.godbolt.org/z/Ejs9a1Thr. GCC/libstdc++ did the full
type-erasing. MSVC did too, but left the "bad variant" case and failed to
eliminate some unnecessary comparisons.
-- Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org Principal Engineer - Intel Data Center - Platform & Sys. Eng.
Received on 2026-04-04 15:32:08
