Why does std::result_of take an (unrelated) function type as a type argument?
Asked Answered
E

4

20

I realize "why are things the way they are" questions are not usually the best, but there are many people on SO that are tuned to standard committee discussions so I hope this can be answered factually, as I'm legitimately curious as to what the answer is.

Basically, it took me a long time to figure out what was going on with std::result_of's template signature the first time I saw it: I thought it was an entirely new construct for template parameters that I had never seen before.

template< class F, class... ArgTypes >
class result_of<F(ArgTypes...)>;

After some time thinking about it, I realized what this actually was: F(ArgTypes...) is a function type, but it's not the type of the function whose result type is being evaluated (that's just F): it's the type of a function taking ArgTypes... arguments and returning type F.

Isn't this...odd? Kind of hackish? Does anyone know if the committee ever discussed any alternatives, like, say, the following...

template< class F, class... ArgTypes >
class result_of<F, ArgTypes...>;

?

I guess it's possible that there's situations where the second construct can't be used as easily as the first one, but which ones?

I'm not trying to pass judgement on this, but it's just that this was legitimately confusing to me the first time I saw it, so I'm curious if there's a good reason for it. I realize part of the answer might simply be "because Boost did it" that way, but still that leave the remaining (factual) questions...

  • Is there a technical reason Boost choose this syntax to encode type information rather than any alternative?

  • Was there any discussion by the C++11 committee about how appropriate it was to standardize this, given that std::result_of can be implemented in terms of decltype fairly easily anyway?

Enthusiasm answered 18/3, 2013 at 21:7 Comment(0)
C
18

Having a function-type as the parameter allows you to have an unrestricted "variadic" class template even in C++03. Think about it: In C++03, we didn't have variadic templates. And you can't "overload" a class template like you can with function templates - so how would it be otherwise possible to allow different amounts of "arguments" to the function?

Using a function type, you can just add any number partial specializations for the different number of parameters:

template<class Fty>
struct result_of;

template<class F>
struct result_of<F()>{ /*...*/ };

template<class F, class A0>
struct result_of<F(A0)>{ /*...*/ };

template<class F, class A0, class A1>
struct result_of<F(A0, A1)>{ /*...*/ };

// ...

The only other way to do this in C++03 is default template arguments and partially specializing for every case - the disadvantage being that it doesn't look like a function call anymore, and that any kind of wrapper that uses result_of internally can't just pass Sig along.


Now, there's one disadvantage with the function-type way - you also get all the usual transformations done to the "parameters": R(Args...) -> R(*)(Args...) and more importantly T[N] -> T* and top-level cv-qualifiers being discarded (§8.3.5/5):

struct X{
  bool operator()(int (&&arr)[3]);
  long operator()(void*);
};

static_assert(std::is_same<std::result_of<X(int[3])>::type, bool>(), "/cry");

Live example. Output:

error: static assertion failed: /cry

The other problems is with the top-level cv-qualifiers being discarded:

struct Y{};

struct X{
  bool operator()(Y const&);
  long operator()(Y&&);
};

Y const f();

static_assert(std::is_same<std::result_of<X(Y const)>::type, bool>(), "/cry");

Live example. Output:

error: static assertion failed: /cry

Choler answered 19/3, 2013 at 1:4 Comment(14)
(but fwiw, I don't think the std::tuple faux varadic approach would have been too hard either, really...I could be wrong though)Enthusiasm
@Stephen: You would have to repeatedly try if the end of your current argument list is _Nil, and if not, append the next argument. For this, you need variadic parameters again. How do you do that in C++03... oh, right, function-type or default arguments. :PCholer
hah well, I see the point...I'm sure it could have been hacked somehow though using default arguments and judicious partial specialization, though. anyway, I guess what's done is done.Enthusiasm
The SFINAE problem is solved for C++14, and in the current draft and by the better implementations, by N3436Kilmer
that is incorrect: c++03 did have array rvalues. when you accessed a member array and the left hand side was an rvalue, the result expression was and is an array rvalue. what it did not have was array temporaries.Prelusive
@Johannes: Ooh, a good point. Hm. This is awkward now. Should I just remove that part? (I think it still applies to some degree, though.)Choler
your testcase for array and pointer conversion is also wrong. an array to pointer conversion is an lvalue transformation. when you have two standard conversion sequences, with one being an lvalue transformation and the other being an identity conversion, like you do have above, you have an ambiguity. so if it wouldnt have transformed the array, it would have given an ambiguityPrelusive
i have not read the entire answer. i just wanted to point out about specific parts. so unfortunately i cannot give good advices :-(Prelusive
@Johannes: Aaaah, I remembered that and correctly changed it in my live example, but forgot to backport the fix to the code in the answer.Choler
I read the spec that std::result_of<T>::type is an SFINAE error if T is not a function type, even if the primary template is left undefined. If the primary template is undefined, it won't be instantiated, and ´std::result_if<T>` is an incomplete type that does not provide ::type. Hence, we here have a name lookup failure and hence the SFINAE condition is met.Prelusive
@Choler errm, I didn't test it. obviously, if implementations still give errors, I suck at understanding stuff and you should ignore me.Prelusive
@Johannes: No worries, you were right, even MSVC gets this one. :)Choler
Hmm...shouldn't it be: static_assert(std::is_same<std::result_of<X(Y const&)>::type, bool>(), "/cry"); which then succeeds?Sheffie
@Tony: Not when the idea is that the argument is a const-qualified rvalue, i.e. a temporary.Choler
C
9

I think it's just that someone got the idea that you could (ab)use the function type notation to mimic the way the respective functor call would look like, and it stuck. So, no technical reasons, just an aesthetic one.

// the result type of a call to (an object of) type F,
// passing (objects of) types A, B, and C as parameters.
result_of<F(A, B, C)>::type
Clorindaclorinde answered 18/3, 2013 at 21:12 Comment(2)
well, are you sure there's no technical reasons? i'd rather believe that there are some to justify this (ab)use...Enthusiasm
+1 This is also mentioned in the rationale here.Skulduggery
P
4

result_of was part of TR1, which came out before decltype was added to the language. But it was designed with decltype in mind, so changing the implementation of result_of to use decltype is simple. Yes, it's a hack, but it works.

Pone answered 18/3, 2013 at 21:9 Comment(7)
can you elaborate? don't you need variadics or faux variadics anyway, to support an arbitrary number of arguments? it's not that I don't believe you, it's just not clear to me how it makes a difference a prioriEnthusiasm
It also just looks neater. std::result_of<S(int, int)> looks like a call of function S with two args of type int.Cima
@KevinBallard yes, but it's not! :DEnthusiasm
@StephenLin - you're right, my answer was hasty. TR1 relied on fake variadics in many places. Edited.Pone
@PeteBecker w.r.t the updated answer, still unclear why you can't just pass F as the first type argument and successive type arguments for parameters, which is kind of consistent with how emplace and bind type functions do things with regular arguments.Enthusiasm
@StephenLin - for what it's worth, emplace and bind are template functions; they get called with actual arguments, and that means they can only be called with a comma-separated list. result_of is used in an entirely different context, namely, template meta-programming, so there's no strong reason to impose function-call-like syntax for it.Pone
@PeteBecker, yes, but I don't think the presumption should be to abuse the type system unless forced not to :D anyway, turns out your first answer was probably the most correct, actually...as per Xeo's answer TR1-style faux varadics would have been harder to do if the variable args were in the main template rather than as partial specializationsEnthusiasm
K
2

(This expands on JohannesD's answer and Jesse Good's comment on it, but this won't fit in a comment. Please upvote that other answer not this one.)

From N1454 Syntax and examples:

The definition of the behavior of result_of is straightforward: given types F, T1, T2, ..., TN and lvalues f, t1, t2, ..., tN of those types, respectively, the type expression

result_of<F(T1, T2, ..., TN)>::type

evaluates to the type of the expression f(t1, t2, ..., tN).

This is not abusing the type system, it's beautifully elegant!

Kilmer answered 19/3, 2013 at 12:29 Comment(1)
Hah, it's only "beautifully elegant" if you think arbitrary token sequence similarities should be allowed to determine expression semantics :D Also, as per @Xeo's answer, if I understand correctly the definition given doesn't always work because of function type decay.Enthusiasm

© 2022 - 2024 — McMap. All rights reserved.