3

[expr.ref]p(6.3.2):

Otherwise, if E1.E2 refers to a non-static member function and the type of E2 is “function of parameter-type-list cv ref-qualifieropt returning T”, then E1.E2 is a prvalue. The expression designates a non-static member function. The expression can be used only as the left-hand operand of a member function call ([class.mfct]). [ Note: Any redundant set of parentheses surrounding the expression is ignored ([expr.prim.paren]). — end note ] The type of E1.E2 is “function of parameter-type-list cv returning T”.

For example the second statement in main below doesn't compile, probably because of the highlighted sentence above. But why is the language set up to work this way?

#include<iostream>
void g();
struct S { void f(); };
S s;

int main(){
    std::cout << "decltype(g) == void() ? " << std::is_same<decltype(g), void()>::value << '\n';            // Ok
    std::cout << "decltype(s.f) == void() ? " << std::is_same<decltype(s.f), void()>::value << '\n';        // Doesn't compile probably because of the sentence hihlighted above in [expr.ref]p(6.3.2).
}
curiousguy
  • 8,038
  • 2
  • 40
  • 58
Belloc
  • 6,318
  • 3
  • 22
  • 52
  • "*But why is the language set up to work this way?*" Exactly what would it be otherwise? `S::f` is not a `void()` function; it's a member function. – Nicol Bolas Jun 01 '19 at 18:37
  • 2
    What do you expect `s.f` to mean, were it valid? – yuri kilochek Jun 01 '19 at 18:38
  • @NicolBolas [\[dcl.type.decltype\]p(1.3)](http://eel.is/c++draft/dcl.type#decltype-1.3) tells exaclty what I would expect for the type to be. – Belloc Jun 01 '19 at 18:44
  • 1
    If you expect `decltype(s.f)` to be the same as `void()`, would you also expect `sizeof(s.f) == sizeof(void())`? Would you expect `auto h = s.f; h();` to work? There's not enough space in a plain function pointer to store information sufficient to make `s.f()` call later. – Igor Tandetnik Jun 01 '19 at 19:14
  • 1
    @IgorTandetnik ["The sizeof operator shall not be applied to an expression that has function or incomplete type, to the parenthesized name of such types, or to a glvalue that designates a bit-field."](http://eel.is/c++draft/expr.sizeof#1.sentence-3) – Belloc Jun 01 '19 at 19:22
  • 1
    I think I would expect `decltype(s.f)` to *not* be the same as `void()`, but that the above code would compile and print `false`... – Chris Dodd Jun 01 '19 at 19:44
  • @ChrisDodd Well, [\[dcl.type.decltype\]p(1.3)] (http://eel.is/c++draft/dcl.type#decltype-1.3) says that `decltype(s.f) == void()` were not for the statement highlighted above, disallowing the use of `s.f` other than when calling a member function `f`. – Belloc Jun 01 '19 at 20:02
  • 2
    @Belloc: Were it not for the statement highlighted, "type of the entity named" would not be `void()`. Maybe "non-static member function of type `struct S` taking no explicit parameters and returning `void`". Maybe some variant with "and bound". (But not "and bound to `s`", because the identity of an object instance is value information not type information) – Ben Voigt Jun 01 '19 at 20:12
  • I would expect `decltype(s.f)` to be some kind of member function type (perhaps something morally equivalent to `void (S::&)()` if that existed), not a function type. – Chris Dodd Jun 02 '19 at 00:23

2 Answers2

3

When you do E1.E2, you are not talking about a general property of the type of thing that E1 is. You're asking to access a thing within the object designated by E1, where the name of the thing to be accessed is E2. If E2 is static, it accesses the class static thing; if E2 is non-static, then it accesses the member thing specific to that object. That's important.

Member variables become the subobject. If your class S had a non-static data member int i;, s.i is a reference to an int. That reference, from the stand point of an int&, behaves no differently from any other int&.

Let me say that more clearly: any int* or int& can point to/reference an int which is a complete object or an int which is a subobject of some other object. The single construct int& can serve double-duty in this way.*

Given that understanding of s.i, what would be the presumed meaning of s.f? Well, it should be similar, right? s.f would be some kind of thing that, when called with params, will be the equivalent of doing s.f(params).

But that is not a thing which exists in C++.

There is no language construct in C++ which can represent that meaning of s.f. Such a construct would need to store a reference to s as well as the member S::f.

A function pointer can't do that. Function pointers need to be able to be pointer-interconvertible with void***. But such an s.f would need to store the member S::f as well as a reference to s itself. So by definition, it'll have to be bigger than a void*.

A member pointer can't do that either. Member pointers explicitly don't carry their this object along with them (that's kind of the point); you must provide them at call-time using the specific member pointer call syntax .* or .->.

Oh, there are ways to encode this within the language: lambdas, std::bind, etc. But there is no language-level construct which has this precise meaning.

Because C++ is asymmetric in this way, where s.i has an encodable meaning but not s.f, C++ makes the unencodable one illegal.

You may ask why such a construct doesn't simply get built. It's not really that important. The language works perfectly fine as is, and due to the complexity of what an s.f would need to be, it's probably best to make you use a lambda (for which admittedly there should be ways to make it shorter to write such things) if that's what you want.

And if you want a naked s.f to be equivalent to S::f (ie: designates the member function), that doesn't really work either. First, S::f doesn't have a type either; the only thing you can do with such a prvalue is convert it to a pointer to a member. Second, a member function pointer doesn't know what object it came from, so in order to use one to call the member, you need to give it s. Therefore, in a call expression, s would have to appear twice. Which is really silly.

*: there are things you can do to complete objects that you cannot do to subobjects. But those provoke UB because they're not detectable by the compiler, because an int* doesn't say if it comes from a subobject or not. Which is the main point; nobody can tell the difference.

**: the standard does not require this, but the standard cannot do something which out-right makes such an implementation impossible. Most implementations provide this functionality, and basically any DLL/SO loading code relies on it. Oh, and it would also be completely incompatible with C, which makes it a non-starter.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • [\[dcl.type.decltype\]p(1.3)(http://eel.is/c++draft/dcl.type#decltype-1.3) says that `decltype(s.f) == void()`, the same way `decltype(g) == void()` in the example. I'm sorry but I can't be more objective than this. AFAICT, the only reason this code doesn't compile is because of the alluded statement in [expr.ref]p(6.3.2). – Belloc Jun 01 '19 at 20:15
  • 2
    @Belloc: Where does it say that? It says "type of the entity named by e.". Well, `s.f` names "a non-static member function". And the type of that non-static member function is not `void()`; that's not how typing works with non-static members. – Nicol Bolas Jun 01 '19 at 20:17
  • I finally got what you're trying to tell me. I was wrong, `decltype(s.f)` would not be equal to `void()`, even if we assumed that the expression `decltype(s.f)` were well-formed. Thanks. – Belloc Jun 01 '19 at 21:17
  • @NicolBolas: `void()` **is** the type of that (defective) expression. – Davis Herring Jun 02 '19 at 02:46
  • 2
    @curiousguy: As a matter of interest, `memcpy` between trivially copyable objects doesn't work if one of them is a base class subobject. This is due to EBO; an empty object type still has a non-zero `sizeof`, but if the base class is subject to EBO, it may overlap with other subobjects. So such copies are forbidden, even though they would be *generally* allowed with that type. Also, C++20's `[[no_unique_address]]` [expands this restriction](https://timsong-cpp.github.io/cppwp/basic.types#3) to properly designated member subobjects too. – Nicol Bolas Jun 04 '19 at 04:10
  • @NicolBolas So you proved that some trivial subobjects cannot have their (address,size) information used as if they were complete objects: those that are base classes and more recently those you designated as not being treatable as complete objects. As `memcpy` is used to copy information, there is no reason (outside generic code maybe) to ever do a bitwise copy a the non empty representation of information free object, which would be inherently less efficient then copying its (zero) members. – curiousguy Jun 04 '19 at 14:03
  • My Q: "what guarantees exist for complete objects don't exist for subobjects" was in the context of "_because an int* doesn't say if it comes from a subobject or not._". Obviously a `int` subobject cannot be a base class; and anything containing one isn't empty and I don't see how integers could overlap in practice. – curiousguy Jun 04 '19 at 14:08
  • 2
    @curiousguy: The point of the footnote is to not leave the reader with the impression that pointers/references to complete objects and subobjects behave 100% identically in all cases. It's not intended to be a declaration that `int*/&` *specifically* will behave differently in some cases, merely preventing the reader from having an incorrect idea about C++ in general. That's why it's a *footnote* and not inline text. So it's best to just treat the statement as it is intended and move on. – Nicol Bolas Jun 04 '19 at 14:23
  • @NicolBolas I'm sorry I misinterpreted the scope of the footnote. We can agree to agree! – curiousguy Jun 04 '19 at 14:34
1

The stat_result.st_mtime syntax was inherited from C, where it always has a value (more particularly, an lvalue). It is therefore syntactically an expression even in the case of a method call, but it has no value because it is evaluated in concert with its associated call expression.

It is (as you quoted) given the type of the member function so as to satisfy the requirement that a function be called via an expression of the correct type. It would, however, be misleading to define decltype for it since it cannot be a full-expression (as is every unevaluated operand) and common expression SFINAE with decltype would not prevent a hard error from the guarded instantiation.

Note that a non-static member function can be named in an unevaluated operand, but that allows S::f (or just f within the class, although that is arguably rewritten to be (*this).f in a member function), not s.f. That expression has the same type but is itself restricted (to appearing with & to form a pointer to member [function]) for the same reason: if it were to be used otherwise, it would be usable as a normal [function] pointer, which is impossible.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • I'm really interested in understanding what you wrote above. But your answer is too terse for my knowledge. Could you be so kind to rewrite it in more simple terms? – WaldB Jun 03 '19 at 13:23
  • @WaldB: I wouldn’t call this *terse*—could you say what it is that you don’t understand? – Davis Herring Jun 04 '19 at 02:59
  • You can't form a pointer to member with `&f` even inside the class. It definitely means `this->f` which as we've already seen (the topic of the whole question) cannot be used in any way except a function call. – Ben Voigt Jun 04 '19 at 03:33
  • @BenVoigt: There’s some confusion on this subject, I think—note that compilers [reject](https://wandbox.org/permlink/0Q3vbaBgQdbQKSGU) a use of a non-static member function’s name even in a context where `this` is not allowed, despite the fact that no implicit `(*this).` should be added there. – Davis Herring Jun 04 '19 at 03:56
  • @DavisHerring: The compiler is correct to generate errors in your link: you can only form a pointer-to-member by using a qualified name. And there is no such thing as a member function type, only a pointer-to-member type so `decltype(S::f)` is also illegal but `decltype(&S::f)` is allowed. – Ben Voigt Jun 04 '19 at 04:16
  • @BenVoigt: Nothing tries to make a pointer-to-member here: (as I linked in the answer) member functions have [normal function types](http://eel.is/c++draft/class.mem#17.note-1). – Davis Herring Jun 04 '19 at 04:30
  • 1
    Yeah.... that note looks like a wording defect. Inspecting the type of a member function (qualified name) may result in a normal function type (useful for template meta-programming), but that's a fiction created by the inspection operation; the member function does not actually conform to that type in any type theory sense. – Ben Voigt Jun 04 '19 at 04:39
  • @BenVoigt: There are even types like `int() const` for const-`this` member functions. They’re certainly not an accident, even though they are extremely strange. – Davis Herring Jun 04 '19 at 04:56