Topic: G++ rererence initialization (std sec 8.5.3/5?)


Author: David Mazieres <dm+stdcxx@amsterdam.lcs.mit.edu>
Date: Thu, 26 Jul 2001 00:50:54 GMT
Raw View
I have a question on reference initialization in C++.  At issue is
whether a particular piece of code is valid C++.  Most compilers seem
to accept the code, but g++ version 3 does not.  A close reading of
the standard suggests g++ 3 may be right.  However, there are two
possible readings of section 8.5.3 paragraph 5 of the standard.  G++ 3
has the more straight-forward interpretation.  In context, however, it
seems strange that the standard's authors would have intended that
reading.  I would like to know which interpretation is correct.

The code in question is the following:

  class foo {
    foo (const foo &) {}
   public:
    foo (int) {}
  };

  void
  foofunc (const foo &f)
  {
  }

  void
  err ()
  {
    foofunc (0);
  }

G++ 3 rejects it with the following errors:

  test.C: In function `void err()':
  test.C:2: `foo::foo(const foo&)' is private
  test.C:15: within this context
  test.C:15:   initializing temporary from result of `foo::foo(int)'
  test.C:9: in passing argument 1 of `void foofunc(const foo&)'

Passing an int to a function taking a "const foo &" requires a
reference copy initialization.  Section 8.5.3 par 5 discusses this.  I
quote from that paragraph:

+ A reference to type ``cv1 T1'' is initialized by an expression of type
+ ``cv2 T2'' as follows:

Okay, so here "cv1 T1" is "const foo", and "cv2 T2" is int.  There are
several cases which do not apply, so I skip to the end of the
paragraph:

+    Otherwise, a temporary of type ``cv1 T1'' is created and
+    initialized from the initializer expression using the rules for a
+    non-reference copy initialization (8.5). The reference is then
+    bound to the temporary. If T1 is reference-related to T2, cv1
+    must be the same cv-qualification as, or greater cv-qualification
+    than, cv2; otherwise, the program is ill-formed. [Example:
+
+               const double& rcd2 = 2;                   // rcd2 refers to temporary with value 2.0
+               const volatile int cvi = 1;
+               const int& r = cvi;                       //  error:  type qualifiers dropped
+
+
+          ---end example]

Okay, so to understand this, we have to look at section 8.5 paragraph
14 for a description of non-reference copy initialization.  The
relevent clause is the following, which describes the copy
initialization, of, say "foo f = 0":

+   Otherwise... user-defined conversion sequences... are enumerated...
+   and the best one is chosen... The function selected is called with the
+   initializer expression as its argument; if the function is a
+   constructor, the call initializes a temporary of the destination
+   type. The result of the call (which is the temporary for the
+   constructor case) is then used to direct-initialize, according to the
+   rules above, the object that is the destination of the
+   copy-initialization.

That means thet the code "foo f = 0" can be treated as:

    foo TEMP (0);
    foo f (TEMP);

This code is indeed illegal, since initializing f from TEMP would
require using the private copy constructor.  For efficiency purposes,
the compiler is allowed to omit the temporary object, but section 12.1
paragraph 1 says that access control still applies:

+ Even when the creation of the temporary object is avoided (12.8),
+ all the semantic restrictions must be respected as if the temporary
+ object was created.

Okay, but in our case we are interested not in "foo f = 0", but rather
a reference copy initialization, "const foo &f = 0".  So back to
8.5.3/5, the question is how to read this sentence:

+    Otherwise, a temporary of type ``cv1 T1'' is created and
+    initialized from the initializer expression using the rules for a
+    non-reference copy initialization (8.5).


*** READING A:

One possible reading is that the temporary of type cv1 T1 is created
just like the temporary in the rules for non-reference copy
initialization.  So where "foo f = 0" gave us this:

    foo TEMP (0);
    foo f (TEMP);

"const foo &f = 0" would translate into:

    foo TEMP (0);
    const foo &f (TEMP).

Given this reading, a "const foo &" can certainly be bound to TEMP,
because the compiler should never call the copy constructor foo::foo
(const foo &).


*** READING B:

The other reading, favored by g++ 3, is that the temporary in 8.5.3/5
itself is initialized by non-reference copy initialization.  In other
words, we get:

    foo TEMP2 = 0;
    const foo &f (TEMP2);

Which then translates into:

    foo TEMP (0);
    foo TEMP2 (TEMP);
    const foo &f (TEMP2);

This then makes the code illegal, because foo::foo (const foo &) is
private.


*** DISCUSSION:

Reading B is the more straight-forward of the two, but other compilers
seem to disagree.  Moreover, it's hard to understand what would have
motivated the standards committee to intend reading B.

Reading A would allow for more programming flexibility.  You could
pass by reference temporary objects that couldn't be copied.  It would
also require more efficient code from the compiler, preventing the
compiler from generating two temporary objects.

Another piece of evidence favoring reading A occurs earlier in
8.5.3/5.  When initializing a "cv1 T1 &" from a "cv2 T2", the standard
distinguishes the case where cv1 T1 is reference-compatible with cv2
T2 (i.e., T1 is T2 or a supertype of T2, or T2 has a method
T2::operator T3 &() where T1 is T3 or a supertype of T3).  In this
case, the standard explicitly states that the copy constructor must be
accessible.  This is presumably to simplify the compiler's job for
cases such as the following:

   struct A {};
   extern A f ();
   const A &a = f();

If an implementation sticks the return object of f in a temporary
stack location, it might well need to copy it to a different place on
return.  However, given reading B, there would be no need to state
this requirement explicitly--reading B would always require that the
copy constructor be accessible.

If anyone can shed light on this question, I would much appreciate it.
If reading A is correct, I would like to try to convince the g++ 3
maintainers to change their compiler, and so would need to assemble a
compelling argument.  If reading B is correct, I am curious as to why
the standard ended up this way.  I don't see any reason for a compiler
to need to generate two temporary objects.

Please CC me on amy replies.  Thanks for any help.

David

---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.research.att.com/~austern/csc/faq.html                ]





Author: Martin von Loewis <loewis@informatik.hu-berlin.de>
Date: Thu, 26 Jul 2001 09:22:28 GMT
Raw View
David Mazieres <dm+stdcxx@amsterdam.lcs.mit.edu> writes:

> Okay, but in our case we are interested not in "foo f = 0", but rather
> a reference copy initialization, "const foo &f = 0".  So back to
> 8.5.3/5, the question is how to read this sentence:
>
> +    Otherwise, a temporary of type ``cv1 T1'' is created and
> +    initialized from the initializer expression using the rules for a
> +    non-reference copy initialization (8.5).
>
>
> *** READING A:
>
> One possible reading is that the temporary of type cv1 T1 is created
> just like the temporary in the rules for non-reference copy
> initialization.  So where "foo f = 0" gave us this:
>
>     foo TEMP (0);
>     foo f (TEMP);
>
> "const foo &f = 0" would translate into:
>
>     foo TEMP (0);
>     const foo &f (TEMP).

In this reading, you would initialize the reference using the rules
for non-reference copy initialization. However, the text in 8.5.3
continues to read

# The reference is then bound to the temporary.

How would this fit into READING A? Here, the temporary that the
reference is bound to clearly is TEMP. However, TEMP was initialized
using direct-initialization; so in this reading, no temporary would be
created using non-reference copy initialization.

> Reading B is the more straight-forward of the two, but other compilers
> seem to disagree.

If they disagree, I guess they do so for completely other
reasons. E.g. g++ 2.95 would accept this code, but *not* because it
took your reading A. Instead, it elided the call to the copy
constructor, and failed to apply 12.1/1. Indeed, the recent fix in g++
is that it correctly implements the constraint in 12.1/1 now. I expect
that many other compilers have the same bug: They perform access
checks only for methods that are actually called, whereas the standard
requires that some methods must be accessible even if they are not
called.

> Moreover, it's hard to understand what would have motivated the
> standards committee to intend reading B.

The hard-to-understand part is why the temporary is initialized using
copy initialization, not using direct-initialization. This I cannot
explain either.

Regards,
Martin

---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.research.att.com/~austern/csc/faq.html                ]





Author: "Pavel Kuznetsov" <pavel_kuznetsov@deadspam.com>
Date: Thu, 26 Jul 2001 16:27:32 GMT
Raw View
"David Mazieres" <dm+stdcxx@amsterdam.lcs.mit.edu> wrote...

[...]

>   class foo {
>     foo (const foo &) {}
>    public:
>     foo (int) {}
>   };
>
>   void foofunc (const foo &f)
>   {
>   }
>
>   void err ()
>   {
>     foofunc (0);
>   }

[8.5.3/5]:

> + A reference to type ``cv1 T1'' is initialized by an expression
> + of type ``cv2 T2'' as follows:

[...]

> + Otherwise, a temporary of type ``cv1 T1'' is created and
> + initialized from the initializer expression using the rules
> + for a non-reference copy initialization (8.5). The reference is
> + then bound to the temporary. [...]

[8.5/14]:

> + Otherwise... user-defined conversion sequences... are
> + enumerated... and the best one is chosen... The function selected
> + is called with the initializer expression as its argument; if the
> + function is a constructor, the call initializes a temporary of
> + the destination type. The result of the call (which is the
> + temporary for the constructor case) is then used to
> + direct-initialize, according to the rules above, the object that
> + is the destination of the copy-initialization.

[...]

> *** READING A:
>
> One possible reading is that the temporary of type cv1 T1 is
> created just like the temporary in the rules for non-reference copy
> initialization.  So where "foo f = 0" gave us this:
>
>   foo TEMP (0);
>   foo f (TEMP);
>
> "const foo &f = 0" would translate into:
>
>   foo TEMP (0);
>   const foo &f (TEMP).
>
> Given this reading, a "const foo &" can certainly be bound to TEMP,
> because the compiler should never call the copy constructor foo::foo
> (const foo &).

> *** READING B:
>
> The other reading, favored by g++ 3, is that the temporary
> in 8.5.3/5 itself is initialized by non-reference copy
> initialization.  In other words, we get:
>
>   foo TEMP2 = 0;
>   const foo &f (TEMP2);
>
> Which then translates into:
>
>   foo TEMP (0);
>   foo TEMP2 (TEMP);
>   const foo &f (TEMP2);
>
> This then makes the code illegal, because foo::foo (const foo &) is
> private.

I suppose that [8.5.3/5] should be interpreted as that the temporary
(the only one) is _both_ created _and_ initialized just like the
temporary in the rules for non-reference copy initialization. And not
that it is _either_ created _or_ initialized as that one in [8.5/14].
The result is the same as in 'reading A'. It seems that the example
from [12.2.5] is applicable here too:

+   class C {
+     //...
+   public:
+     C();
+     C(int);
+     friend C operator+(const C&, const C&);
+     ~C();
+   };
+
+   C obj1;
+   const C& cr = C(16)+C(23);
+   C obj2;
+
+ the expression C(16)+C(23) creates three temporaries. A first
+ temporary T1 to hold the result of the expression C(16), a second
+ temporary T2 to hold the result of the expressionC(23), and a third
+ tempo-rary T3 to hold the result of the addition of these two
+ expressions. The temporary T3 is then bound to the reference cr.

Nothing is said about copying T3 to T4 there...

--
Pavel Kuznetsov

[ DON'T MAIL THE REPLY ADDRESS! THAT IS UNATTENDED MAILBOX.    ]
[ If you do want contact me in person, please use the address: ]
[ pavel_kuznetsov@no.spam.users.sf.net (remove "no.spam.")     ]



---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.research.att.com/~austern/csc/faq.html                ]





Author: "Anthony Williams" <anthwil@nortelnetworks.com>
Date: Thu, 26 Jul 2001 17:32:11 GMT
Raw View
"David Mazieres" <dm+stdcxx@amsterdam.lcs.mit.edu> wrote in message
news:200107260042.f6Q0ggp18479@amsterdam.lcs.mit.edu...
> I have a question on reference initialization in C++.  At issue is
> whether a particular piece of code is valid C++.  Most compilers seem
> to accept the code, but g++ version 3 does not.  A close reading of
> the standard suggests g++ 3 may be right.  However, there are two
> possible readings of section 8.5.3 paragraph 5 of the standard.  G++ 3
> has the more straight-forward interpretation.  In context, however, it
> seems strange that the standard's authors would have intended that
> reading.  I would like to know which interpretation is correct.
>
> The code in question is the following:
>
>   class foo {
>     foo (const foo &) {}
>    public:
>     foo (int) {}
>   };
>
>   void
>   foofunc (const foo &f)
>   {
>   }
>
>   void
>   err ()
>   {
>     foofunc (0);
>   }
>
> G++ 3 rejects it with the following errors:
>
>   test.C: In function `void err()':
>   test.C:2: `foo::foo(const foo&)' is private
>   test.C:15: within this context
>   test.C:15:   initializing temporary from result of `foo::foo(int)'
>   test.C:9: in passing argument 1 of `void foofunc(const foo&)'
>
> Passing an int to a function taking a "const foo &" requires a
> reference copy initialization.  Section 8.5.3 par 5 discusses this.  I

[SNIP discussion of 8.5.3p5]

*** READING A:
>
> One possible reading is that the temporary of type cv1 T1 is created
> just like the temporary in the rules for non-reference copy
> initialization.  So where "foo f = 0" gave us this:
>
>     foo TEMP (0);
>     foo f (TEMP);
>
> "const foo &f = 0" would translate into:
>
>     foo TEMP (0);
>     const foo &f (TEMP).
>
> Given this reading, a "const foo &" can certainly be bound to TEMP,
> because the compiler should never call the copy constructor foo::foo
> (const foo &).
>
>
> *** READING B:
>
> The other reading, favored by g++ 3, is that the temporary in 8.5.3/5
> itself is initialized by non-reference copy initialization.  In other
> words, we get:
>
>     foo TEMP2 = 0;
>     const foo &f (TEMP2);
>
> Which then translates into:
>
>     foo TEMP (0);
>     foo TEMP2 (TEMP);
>     const foo &f (TEMP2);
>
> This then makes the code illegal, because foo::foo (const foo &) is
> private.
>
>
> *** DISCUSSION:
>
> Reading B is the more straight-forward of the two, but other compilers
> seem to disagree.  Moreover, it's hard to understand what would have
> motivated the standards committee to intend reading B.
>
> Reading A would allow for more programming flexibility.  You could
> pass by reference temporary objects that couldn't be copied.  It would
> also require more efficient code from the compiler, preventing the
> compiler from generating two temporary objects.
>
> Another piece of evidence favoring reading A occurs earlier in
> 8.5.3/5.  When initializing a "cv1 T1 &" from a "cv2 T2", the standard
> distinguishes the case where cv1 T1 is reference-compatible with cv2
> T2 (i.e., T1 is T2 or a supertype of T2, or T2 has a method
> T2::operator T3 &() where T1 is T3 or a supertype of T3).  In this
> case, the standard explicitly states that the copy constructor must be
> accessible.  This is presumably to simplify the compiler's job for
> cases such as the following:
>
>    struct A {};
>    extern A f ();
>    const A &a = f();
>
> If an implementation sticks the return object of f in a temporary
> stack location, it might well need to copy it to a different place on
> return.  However, given reading B, there would be no need to state
> this requirement explicitly--reading B would always require that the
> copy constructor be accessible.
>
> If anyone can shed light on this question, I would much appreciate it.
> If reading A is correct, I would like to try to convince the g++ 3
> maintainers to change their compiler, and so would need to assemble a
> compelling argument.  If reading B is correct, I am curious as to why
> the standard ended up this way.  I don't see any reason for a compiler
> to need to generate two temporary objects.

I believe reading B is correct. This makes it consistent with the previous
bullet regarding rvalues of class type, which may be copied an arbitrary
number of times before being finally bound to the reference. This makes
sense, as the temporary generated by the conversion constructor is indeed an
rvalue of class type.

Interestingly, Comeau C++ accepts your example but rejects

class foo {
    foo (const foo &) {}
public:
     foo (int) {}
};

const foo& f=0;

I think Comeau C++ is correct to reject my example, and it is a bug for it
to accept yours as the same initialization is being done.
G++ 3 rejects my example too (as it should).

> Please CC me on amy replies.  Thanks for any help.

Done

Anthony
--
Anthony Williams
Software Engineer, Nortel Networks Optoelectronics
The opinions expressed in this message are not necessarily those of my
employer



---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.research.att.com/~austern/csc/faq.html                ]