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?

Stephen Lin
  • 5,470
  • 26
  • 48

4 Answers4

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

Xeo
  • 129,499
  • 52
  • 291
  • 397
  • (but fwiw, I don't think the `std::tuple` faux varadic approach would have been too hard either, really...I could be wrong though) – Stephen Lin Mar 19 '13 at 01:50
  • @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. :P – Xeo Mar 19 '13 at 01:53
  • 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. – Stephen Lin Mar 19 '13 at 01:54
  • 1
    The SFINAE problem is solved for C++14, and in the current draft and by the better implementations, by [N3436](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3436.html) – Jonathan Wakely Mar 19 '13 at 12:30
  • 1
    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. – Johannes Schaub - litb Mar 19 '13 at 19:23
  • @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.) – Xeo Mar 19 '13 at 19:24
  • 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 ambiguity – Johannes Schaub - litb Mar 19 '13 at 19:29
  • i have not read the entire answer. i just wanted to point out about specific parts. so unfortunately i cannot give good advices :-( – Johannes Schaub - litb Mar 19 '13 at 19:32
  • @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. – Xeo Mar 19 '13 at 19:32
  • I read the spec that `std::result_of::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` is an incomplete type that does not provide `::type`. Hence, we here have a name lookup failure and hence the SFINAE condition is met. – Johannes Schaub - litb Mar 20 '13 at 14:45
  • @Xeo errm, I didn't test it. obviously, if implementations still give errors, I suck at understanding stuff and you should ignore me. – Johannes Schaub - litb Mar 20 '13 at 15:10
  • @Johannes: No worries, [you were right](https://ideone.com/Jrfm65), even MSVC gets this one. :) – Xeo Mar 20 '13 at 15:25
  • Hmm...shouldn't it be: `static_assert(std::is_same::type, bool>(), "/cry");` which then succeeds? – Tony Apr 04 '13 at 05:22
  • @Tony: Not when the idea is that the argument is a const-qualified rvalue, i.e. a temporary. – Xeo Apr 04 '13 at 06:50
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
JohannesD
  • 13,802
  • 1
  • 38
  • 30
  • well, are you sure there's no technical reasons? i'd rather believe that there are some to justify this (ab)use... – Stephen Lin Mar 18 '13 at 21:15
  • 2
    +1 This is also mentioned in the [rationale here](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1454.html). – Jesse Good Mar 18 '13 at 21:21
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.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • 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 priori* – Stephen Lin Mar 18 '13 at 21:10
  • 2
    It also just looks neater. `std::result_of` *looks like* a call of function `S` with two args of type `int`. – Lily Ballard Mar 18 '13 at 21:11
  • @KevinBallard yes, but it's not! :D – Stephen Lin Mar 18 '13 at 21:11
  • @StephenLin - you're right, my answer was hasty. TR1 relied on fake variadics in many places. Edited. – Pete Becker Mar 18 '13 at 21:11
  • @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. – Stephen Lin Mar 18 '13 at 21:16
  • 1
    @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. – Pete Becker Mar 18 '13 at 21:38
  • @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 specializations – Stephen Lin Mar 19 '13 at 01:53
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!

Community
  • 1
  • 1
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 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. – Stephen Lin Mar 19 '13 at 17:12