Custom literal operators for aggregate-initialization syntax with variadic arguments

90 views
Skip to first unread message

Anthony Hall

unread,
Dec 5, 2016, 2:10:51 AM12/5/16
to ISO C++ Standard - Future Proposals
What if there were an additional parameter list allowed for defining custom literals, specifically:

template< typename ...Args > <return_type> operator ""_<suffix> ( Args &&... ) { /*...*/ }

This syntax for invoking this literal operator would use an aggregate-initialization block, such as would be used to initialize a struct.  So for example, this could be used to define a terse suffix for std::tuple:

template< typename ...Args > auto operator ""_tuple ( Args &&...args ) {
    return tuple( std::forward<Args>( args )... ); // Relies on new class template argument deduction from C++17
}
// ...
auto tup = { 1.0f, "unnamed", '\n' }_tuple;

I realize that this particular example might not be particularly motivating, since we already have std::make_tuple, and the new class template arg deduction feature alongside that means that both give nice terse syntax for creating a tuple.  I use it here mostly because it provides a straightforward way to express the idea.  Another example could offer a natural ordered-pair-like syntax for a custom linear algebra Vector class:

template< typename T, size_t N >
struct Vector : std::array<T, N> {
    // ...
};

template< typename T, typename S, size_t N >
Scalar dot( Vector<T, N> const &v1, Vector<S, N> const &v2 ) {
    // compute and return dot product of v1 and v2...
}

template< typename ...Args > auto operator ""_v ( Args &&...args ) {
    return Vector<std::common_type_t<Args...>, sizeof...(Args)>{ std::forward<Args>( args )... };
}
// ...
auto vec3f = {0.74f, 0.74f, 0.0f}_v;
float s = dot( vec3f, {1.0f, 0.0f, 0.0f}_v );

Of course, the above could be achieved with the currently permissible 'char *' parameter for custom literal operators, but that would require some tricky compile-time parsing of the char * string to determine the Vector's template arguments T and N, as well as other compile-time parsing, and the resulting syntax doesn't seem quite as natural or as close to the common mathematics notation for ordered pairs:

auto vec3f = "0.74f, 0.74f, 0.0f"_v;
float s = dot( vec3f, "1.0f, 0.0f, 0.0f"_v );

I also realize the above could also be achieved using normal template functions or 'make_vector" functions, and in a pinch if terseness is desired the make_ functions could just be given very short names.  But pointing that out would serve as an argument against custom literal operators in general, not only this specific extension to them.

Another objection I could anticipate is if this could maybe be seen as another gateway towards the objection compiler implementors have had against the proposed compile-time string literals which return each string character as a separate 'char' non-type template argument, namely that of encouraging the use of too many template arguments which can slow compilation.  But at least for that specific use case I don't see this likely to lead to the same result; it wouldn't make constructing such templates any more convenient than it is presently, since it would still require listing separate characters separately:

auto constexpr s = {'s', 't', 'r', 'i', 'n', 'g', '\0'}_string_literal;

So, does this seem to others like a useful extension to the custom user literals mechanism?  I personally quite like, in the Vector example, how it would enable a notation quite close to traditional math notations.  It could make life easier for any custom type that is convenient to express tersely as some ordered heterogeneous tuple: the compiler would provide type checking and automatic separation of the different arguments without requiring compile-time string parsing.

-Andy

Anthony Hall

unread,
Dec 5, 2016, 2:44:25 AM12/5/16
to ISO C++ Standard - Future Proposals
After writing all of the above, I began to realize all my examples had a variadic template parameter list.  It occurred to me that level of genericness might not always be desired.  For example, with my Vector class, there could be a suffix that enforced a certain scalar type and dimension, with compile-type errors if the suffix is used but the wrong scalar type and dimension are given.  This would be a natural consequence of the compiler simply failing to find an overload of the custom literal operator that matches:

Vector<float, 3> operator ""_v3f ( float s1, float s2, float s3 ) {
   
return Vector<float, 3>{s1, s2, s3};
}
auto vec3 = {0.74f, 0.74f, 0.0f, 1.0f}_v3f // error: no overload of operator ""_v3f takes four arguments


So I realized what I'm proposing, (unless the variadic template signature were explicitly required by the standard), is essentially that the restriction to only a certain set of accepted parameter signature be lifted.  That gives me some pause, since I assume that committee did a lot of work to come up with that specific set of allowable parameter signatures and that there were good reasons for it.  Still, it would be fun if the more mathematical kinds of notation this would allow are as appealing to others as they would be to me, so that there might be some motivation to revisit the reasons for those restrictions.

I wonder if arbitrary function signatures were indeed considered by the committee.  If so, had one of the issues been how to accommodate multiple arguments with a single invoking suffix?  Had the brace-enclosed-initializer syntax I've suggested here been looked at?  Was it rejected on ground that it would complicated parsing?  Since I wasn't involved at all, I'm just speculating wildly; I'm really just trying to anticipate objections or problems there could be so I can think about them.

Warning, from here on, I get very out-there and general in exploring ramifications of this proposal, in a rambling stream of conscious:

Another funny thing about this idea is that, with permitting arbitrary function signatures for literal operators, it would really amount to little more than an alternate suffix-based notation for defining and invoking ordinary free functions; the chief difference being that the argument list is enclosed with braces {} rather than parentheses (), and the function name, obviously, follows the argument list.  I do like the {} for enclosing the list, as braces historically carry more of a connotation of initialization or aggregate data, which is much closer to the expected use-case for user-defined literals.  Is there a reason to allow some free functions to be defined as invokable only with prefix-with-parentheses notation and others only with braces-with-suffix?  If more general use for this tool were found later, would be yet another headache for generic library programmers, to accommodate yet another function invocation syntax (for example, proposals having to do with overloading operator . or introducing a universal member function calling syntax seem to be efforts designed to overcome that kind of issue with function invocation in generic code).  If so, could that be resolved by permitting _all_ functions to be called either by the prefix-based or the suffix-based notation?  I.e. what if some classical function 'int foo(char c)' that's been around for a long time could not be invoked with 'auto t = {'s'}_foo'?  Or, if 'bar' were declared: 'int operator ""_bar (char c);', it could also be invoked by 'auto t = bar('s')'.  Hmm... I'm not sure that would be good at all, since it could suddenly introduce name collisions and unintended overload set expansions to existing code, just because some literal suffix operator was added in some header that happened to use a suffix that matched some other function's name...

It would be hard to argue that all of the above is worth resolving for a feature that amounts to syntactic sugar, and that doesn't really enable any new language functionality.  Maybe it really would be best to not try to marry a '{a, b, c}_suffix' mechanism with the general notion of free functions.  So it's probably best to just stick with trying to fulfill the better-scoped use case of convenient math-like notation for initializations.

-Andy

Ivan G.

unread,
Dec 5, 2016, 6:08:41 AM12/5/16
to ISO C++ Standard - Future Proposals
Why not use a single std::initializer_list<T> argument instead of variadic argument list? 

понедельник, 5 декабря 2016 г., 10:10:51 UTC+3 пользователь Anthony Hall написал:

Nicol Bolas

unread,
Dec 5, 2016, 11:46:01 AM12/5/16
to ISO C++ Standard - Future Proposals


On Monday, December 5, 2016 at 2:10:51 AM UTC-5, Anthony Hall wrote:
What if there were an additional parameter list allowed for defining custom literals, specifically:

template< typename ...Args > <return_type> operator ""_<suffix> ( Args &&... ) { /*...*/ }

This syntax for invoking this literal operator would use an aggregate-initialization block, such as would be used to initialize a struct.  So for example, this could be used to define a terse suffix for std::tuple:

template< typename ...Args > auto operator ""_tuple ( Args &&...args ) {
    return tuple( std::forward<Args>( args )... ); // Relies on new class template argument deduction from C++17
}
// ...
auto tup = { 1.0f, "unnamed", '\n' }_tuple;

I realize that this particular example might not be particularly motivating, since we already have std::make_tuple, and the new class template arg deduction feature alongside that means that both give nice terse syntax for creating a tuple.  I use it here mostly because it provides a straightforward way to express the idea.  Another example could offer a natural ordered-pair-like syntax for a custom linear algebra Vector class:

template< typename T, size_t N >
struct Vector : std::array<T, N> {
    // ...
};

Wasn't there a discussion somewhere about template deduction guides and arrays? Some question about whether or not one could make it work for `std::array`, where it could deduce the number of elements from the number of parameters?

I think it looked something like this:

template<typename ...Args>
std
::array(Args &&...args) -> std::array</*extract the T type*/, sizeof...(Args)>;

This would work even though `array` is an aggregate. The deduction guide part is only used to determine the template parameters; the actual initialization happens as normal.

Thiago Macieira

unread,
Dec 5, 2016, 4:15:06 PM12/5/16
to std-pr...@isocpp.org
Em domingo, 4 de dezembro de 2016, às 23:10:51 PST, Anthony Hall escreveu:
> auto vec3f = {0.74f, 0.74f, 0.0f}_v;
> float s = dot( vec3f, {1.0f, 0.0f, 0.0f}_v );

Why can't you write:

auto vec3f = v(0.74f, 0.74f, 0.f);
float s = dot(vec3f, v{1.0f, 0.0f, 0.0f});

With a suitably-defined "v" function somewhere that operates like make_tuple?

Why does it need to be an UDL?

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

Anthony Hall

unread,
Dec 5, 2016, 4:59:24 PM12/5/16
to std-pr...@isocpp.org
Why can't you write:
        auto vec3f = v(0.74f, 0.74f, 0.f);
        float s = dot(vec3f, v{1.0f, 0.0f, 0.0f});
With a suitably-defined "v" function somewhere that operates like make_tuple?
Why does it need to be an UDL?

I did acknowledge that exact possibility (a 'make'-like function with a terse name) as a current workaround; the essence of my response was that much the same argument could be made against even having the feature of UDLs in the language, but that's a done deal, so evidently the committee did see some use for suffixes as opposed to named constructors or make- functions for at least simple user-defined types.  I could understand, though, if introducing the more general brace-initilaizer syntax takes it outside the intended scope and motivation for UDLs.

I realize the examples I gave weren't very motivating for the feature of having a brace-initializer syntax for UDLs, with arbitrary type arguments.  The Vector example, for instance, has viable workarounds in the current standard, as responses thus far have shown.

I am still curious though, aside from solving the particular examples I gave, what the objections would be to the proposed extensions to the UDL facility itself.  But maybe the point of the responses is that, given the examples, we don't see sufficient motivation for it.

-Andy


--
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/BZ5kC9_B3i8/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/1761696.CnmSmY9c4V%40tjmaciei-mobl1.

Reply all
Reply to author
Forward
0 new messages