Why doesn't C++ support strongly typed ellipsis?
Asked Answered
B

8

28

Can someone please explain to me why C++, at least to my knowledge, doesn't implement a strongly typed ellipsis function, something to the effect of:

void foo(double ...) {
 // Do Something
}

Meaning that, in plain speak: 'The user can pass a variable number of terms to the foo function, however, all of the terms must be doubles'

Biogeochemistry answered 28/8, 2015 at 12:10 Comment(14)
I would guess that variadic functions were added to C with the sole purpose of supporting the printf family of functions, which must be type-unsafe. The format-string I/O concept itself was probably just taken from C's predecessors like BCPL (see en.wikipedia.org/wiki/BCPL). In modern C++, there is no need to introduce type-safe variadic functions, because we have superior language constructs anyway, especially since C++11. Unfortunately I have no references for my guesses. It would be interesting to ask this question to Bjarne Stroustrup himself.Arietta
You can do void foo(double *) and call it by foo((double[]){1,2,3,4,5}). Need GNU C++ extension.Equities
Is'nt that feature too anecdotic to be worth incorporating in an already overfeatured language ? Then you should as well claim void foo(double ..., int ..., double ...) and the like.Sycamine
@ChristianHackl: There's no fundamental reason why the printf family must be type-unsafe. C could have declared that the implementation first pushes a "type token" on the call stack so the vararg mechanism can check that the right type of value is on the stack. That would have slowed down correct code, and C historically had a strong preference of fast above safe.Transmissible
@MSalters: OTOH, that still would not make it type-safe at compile time.Arietta
@YvesDaoust Actually, I think that would be pretty useful...Biogeochemistry
@Transmissible I think gcc prints a warning when printf/scanf argument is mismatched to the format specifier, I'm sure I've seen it.Brophy
@nicholashamilton: I managed to program in C and C++ for 25 years without ever feeling the need for a variadic function.Sycamine
@YvesDaoust yeah well, the convenience of functional languages....Biogeochemistry
@sashoalm: Indeed. It only works if the string is a constant, though. Which is precisely why printf as specified cannot be safe at compile time.Transmissible
@Transmissible It does show how a little pragmatism could have been a great help, though - this warning could have been incorporated into K&R C easily (it's not much of a technical challenge), and would have covered 99.99% of the bugs. Instead they wanted to be purists and not allow special treatment for printf/scanf.Brophy
@Equities template<class T> using id = T; void foo(double*); foo(id<double[]>{1,2,3,4}); works fine w/o extensions.Disincentive
@MSalters: A type-safe version could probably have been about as cheap and in many cases cheaper than the hack that was implemented if the print function took a compiler-generated const char* describing the parameters and compilers could choose the passing means as appropriate, so given int a; long b; print(1, 0x12345, a, b, a+b) a compiler could generate a temp holding a+b, and pass a static const char[] containing, encoded, the integer value 1, the integer value 0x12345, the static or frame-relative address of a, the static or frame-relative address of b, and the frame-relative...Abbyabbye
...address of the compiler temp value. In most systems in the early days of C, pushing an two-byte integer value stored in a variable would take 4 bytes of code and pushing a long would often take twice that, but a fairly simple encoding method could reduce the overhead of pushing an int to two bytes (or maybe even one) if the variable was near the top of the stack frame, as would often be the case.Abbyabbye
T
15

Historically, the ellipsis syntax ... comes from C.

This complicated beast was used to power printf-like functions and is to be used with va_list, va_start etc...

As you noted, it is not typesafe; but then C is far from being typesafe, what with its implicit conversions from and to void* for any pointer types, its implicit truncation of integrals/floating point values, etc...

Because C++ was to be as close as possible as a superset of C, it inherited the ellipsis from C.


Since its inception, C++ practices evolved, and there has been a strong push toward stronger typing.

In C++11, this culminated in:

  • initializer lists, a short-hand syntax for a variable number of values of a given type: foo({1, 2, 3, 4, 5})
  • variadic templates, which are a beast of their own and allow writing a type-safe printf for example

Variadic templates actually reuse the ellipsis ... in their syntax, to denote packs of types or values and as an unpack operator:

void print(std::ostream&) {}

template <typename T, typename... Args>
void print(std::ostream& out, T const& t, Args const&... args) {
    print(out << t, args...); // recursive, unless there are no args left
                              // (in that case, it calls the first overload
                              // instead of recursing.)
}

Note the 3 different uses of ...:

  • typename... to declare a variadic type
  • Args const&... to declare a pack of arguments
  • args... to unpack the pack in an expression
Tim answered 28/8, 2015 at 14:32 Comment(3)
The call to print doesn't make much sense. Where do the args go ?Lineal
@Quentin: Variadic functions often work (today) with recursion, so you call print with 4 arguments, which calls print with 3 arguments, which calls print with 2 arguments, which calls print with 1 argument => this is the base case (non-variadic function) and the recursion stops.Tim
I'm blind, didn't see that they were two overload of print. Makes way more sense now :pLineal
D
24

There is

 void foo(std::initializer_list<double> values);
 // foo( {1.5, 3.14, 2.7} );

which is very close to that.

You could also use variadic templates but it gets more discursive. As for the actual reason I would say the effort to bring in that new syntax isn't probably worth it: how do you access the single elements? How do you know when to stop? What makes it better than, say, std::initializer_list?

C++ does have something even closer to that: non-type parameter packs.

template < non-type ... values>

like in

template <int ... Ints>
void foo()
{
     for (int i : {Ints...} )
         // do something with i
}

but the type of the non-type template parameter (uhm) has some restrictions: it cannot be double, for example.

Dyadic answered 28/8, 2015 at 12:42 Comment(2)
and the value of the non-type template parameter(s) have restrictions -- they have to be compile-time constant expressions.Lithesome
I needed something like the question, I thought my only option was variadic function but this is the best option. Except the caller needs an extra two curly braces.Partite
T
15

Historically, the ellipsis syntax ... comes from C.

This complicated beast was used to power printf-like functions and is to be used with va_list, va_start etc...

As you noted, it is not typesafe; but then C is far from being typesafe, what with its implicit conversions from and to void* for any pointer types, its implicit truncation of integrals/floating point values, etc...

Because C++ was to be as close as possible as a superset of C, it inherited the ellipsis from C.


Since its inception, C++ practices evolved, and there has been a strong push toward stronger typing.

In C++11, this culminated in:

  • initializer lists, a short-hand syntax for a variable number of values of a given type: foo({1, 2, 3, 4, 5})
  • variadic templates, which are a beast of their own and allow writing a type-safe printf for example

Variadic templates actually reuse the ellipsis ... in their syntax, to denote packs of types or values and as an unpack operator:

void print(std::ostream&) {}

template <typename T, typename... Args>
void print(std::ostream& out, T const& t, Args const&... args) {
    print(out << t, args...); // recursive, unless there are no args left
                              // (in that case, it calls the first overload
                              // instead of recursing.)
}

Note the 3 different uses of ...:

  • typename... to declare a variadic type
  • Args const&... to declare a pack of arguments
  • args... to unpack the pack in an expression
Tim answered 28/8, 2015 at 14:32 Comment(3)
The call to print doesn't make much sense. Where do the args go ?Lineal
@Quentin: Variadic functions often work (today) with recursion, so you call print with 4 arguments, which calls print with 3 arguments, which calls print with 2 arguments, which calls print with 1 argument => this is the base case (non-variadic function) and the recursion stops.Tim
I'm blind, didn't see that they were two overload of print. Makes way more sense now :pLineal
L
11

It is already possible with variadic templates and SFINAE :

template <bool...> struct bool_pack;
template <bool... v>
using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;

template <class... Doubles, class = std::enable_if_t<
    all_true<std::is_convertible<Doubles, double>{}...>{}
>>
void foo(Doubles... args) {}

Thanks to Columbo for the nice all_true trick. You will also be able to use a fold expression in C++17.

As later and upcoming standards are focusing on terser syntax (terse for-loops, implicit function templates...) it is very possible that your proposed syntax ends up in the Standard one day ;)

Lineal answered 28/8, 2015 at 13:4 Comment(4)
I can't get it to work: error C2783: 'void foo(Doubles...)': could not deduce template argument for '<unnamed-symbol>'Lacylad
@SimonKraemer IIRC SFINAE is all kinds of broken on MSVC, but sadly I don't have experience in finding workarounds...Lineal
Even in VS2015? Ok.... In the meantime I build a "workaround" myself: https://mcmap.net/q/489778/-why-doesn-39-t-c-support-strongly-typed-ellipsisLacylad
@RyanHaining haha, yeah, C++ often starts with the most complete possible syntax, then adds shorthands for recurrent patterns. If you look at a C++03 iterator for-loop, it's pretty bulky too.Lineal
N
2

For why specifically such a thing wasn't proposed (or was proposed and rejected), I do not know. Such a thing would certainly be useful, but would add more complexity to the language. As Quentin demonstrates, there is already proposes a C++11 way of achieving such a thing with templates.

When Concepts gets added to the standard, we'll have another, more concise way:

template <Convertible<double>... Args>
void foo(Args... doubles);

or

template <typename... Args>
    requires Convertible<Args, double>()...
void foo(Args... doubles);

or, as @dyp points out:

void foo(Convertible<double>... doubles);    

Personally, between the current solution and the ones that we will get with Concepts, I feel that's an adequate solution to the problem. Especially since the last one is basically what you'd originally asked for anyway.

Neldanelia answered 28/8, 2015 at 14:19 Comment(7)
Or simply void foo(Convertible<double>... doubles); (which I had to look up, but is guaranteed to work by N4377; the reason probably being that any function with a placeholder in the parameter-decl-clause is equivalent to / defined as a function template)Disincentive
@Disincentive Personally I'm not a fan of that usage as now we're hiding that this is a function template. Although I look forward to all the rep I will get by answering SO questions about it :)Neldanelia
Oh we can make it entirely clear that this is a template by using the pretty AllConvertibleToDouble{...T} void foo(T... t); -- I just can't get gcc to accept AllConvertible<double>{...T} void foo(T... t); unfortunately :( -- hmm now I want to define a concept named TemplateDisincentive
@Disincentive AllConvertible<double>{...T} is invalid syntax: a template-introduction needs a qualified-concept-name before the {. AllConvertible<double> is a partial-concept-id.Guacin
Also, FWIW, Convertible is likely going to be named ConvertibleTo in accordance with LWG guidance during the July Ranges TS telecon. (Unless, obviously, the committee changes the name again ;)Guacin
@Guacin Not sure I can follow you: A qualified-concept-name contains a constrained-type-name which can be a partial-concept-id according to [dcl.spec.auto.constr] .... ? -- edit: oh, and I see a xkcd.com/541 there ;)Disincentive
@Disincentive I had convinced myself that qualified-concept-name := nested-name-specifier_opt concept-name, which is very much incorrect. In any case, the concept resolution rules in [temp.constr.resolve] don't correctly specify how to form the concept argument list for a partial-concept-id used in a template introduction. I've filed issue #92 against the concepts TS to fix it.Guacin
P
1

The way to achieve (sort of) what you suggest is to use variadic templates

template<typename... Arguments>
void foo(Arguments... parameters);

however you can pass any type in the parameter pack now. What you propose has never been implemented, maybe it could be a great addition to the language, or it could just be too difficult to implement as things stand. You could always try to write a proposal and submit it to isocpp.org

Parochialism answered 28/8, 2015 at 12:38 Comment(1)
It's not too difficult to implement. It's in fact easier than variadic templates. However, the paper should (A) include a rationale why this is needed, and thus why std::initializer_list<double> won't do, and probably (B) explain how sizeof... works for double.... Bonus points for (C) actual proposed wording for the Standard.Transmissible
L
1
template<typename T, typename... Arguments>
struct are_same;

template <typename T, typename A1, typename... Args>
struct are_same<T, A1, Args...>{    static const bool value = std::is_same<T, A1>::value && are_same<T, Args...>::value;};

template <typename T>
struct are_same<T>{static const bool value = true;};

template<typename T, typename... Arguments>
using requires_same = std::enable_if_t<are_same<T, Arguments...>::value>;

template <typename... Arguments, typename = requires_same<double, Arguments...>>
void foo(Arguments ... parameters)
{
}
Lacylad answered 28/8, 2015 at 14:54 Comment(0)
C
1

Based on Matthew's answer:

void foo () {}

template <typename... Rest>
void foo (double arg, Rest... rest)
{
    /* do something with arg */
    foo(rest...);
}

If the code using foo compiles, you know all the arguments are convertible to double.

Customary answered 28/8, 2015 at 18:42 Comment(0)
H
0

Because you can use

void foo(std::vector<T> values);
Hedwighedwiga answered 28/8, 2015 at 12:13 Comment(5)
...which is type-safe but only allows a single type... and is not very "natural" to call, either.Gershon
I seriously doubt this is the reason. Plus, it is not the same kind of thing at all.Glennglenna
@juan, please elaborate. The OP wanted an analogue of a function with a variable amount of arguments, with all the arguments being of a single type (that is clearly stated). Now, what do we have here, compared to that? A variable amount of unnamed arguments? Check. Being able to count the arguments actually passed, and get their values? Check. Of course, this is not the reason, however, I think that a function that takes a vector of T fully meets the OPs requirement of The user can pass a variable number of terms to the foo function, however, all of the terms must be of type THedwighedwiga
@SingerOfTheFall: To me, it sounds more like the OP wants some historical references or other theoretical background on why there is no built-in language support for such a feature.Arietta
@MSalters: Well, it sure feels very different to me than foo(0.4, 0.3, 0.2);. But feelings are irrational anyway :)Arietta

© 2022 - 2024 — McMap. All rights reserved.