Topic: C++ Standards Question: Conversion Functions


Author: Bob Hathaway <rjh@geodesic.com>
Date: 1995/04/20
Raw View
In article <D7Azt3.LMB@midway.uchicago.edu>,
Bob Hathaway  <rjh@geodesic.com> wrote:
>In article <AKV.95Apr11093358@srl03.cacs.usl.edu>,
>Anil Vijendran <akv@cacs.usl.edu> wrote:
>>
>>> On Fri, 31 Mar 1995 19:22:50 GMT, Bob Hathaway <rjh@geodesic.com> said:
>>>...
>>...
>...
>  are not aware (but this is an error), and that a change to this stealth
>  (which should never occur) conversion interface would change users
>  code (simply incorrect in the direct case] - but so what if the user's
>  defect is mildly changed; its still wrong, whether thru an incorrect
>  constructor or the addition or change of several!   Admittedly, I am

I think I've finally thought of the *only* two real cases where a problem
can occur.  I now ignore the unknown classes error pointed out in the C++
Report because a competent programmer will correctly choose his arguments
(and know his classes!)  These changes are similar to changes in conversion
functions, but I'll cover them anyway, so if the following change(s) occur
in a derived class of the formal then no error or waring will occur (and
these are the only two cases):
  1) The original ctor is deleted and simultaneously *exactly* one matching
     ctor is added (probably along a different inheritance path).
  2) Another ctor is added along the same inheritance path.

CASE 1) This is obscure, but is it a problem?

       person
        /  \
      boy  girl -- girl(spirit&)  // A reincarnation constructor, girls can be
                                  // reincarnated.

... takesToDinner(person&);       // A declaration that accepts a person that
                                  // can be taken to dinner.

spirit  freddyKruger;             // Freddy is a spirit.

... takesToDinner(freddyKruger);  // Freddy can become a girl and go to dinner
                                  // (Yikes!  Luckily this hasn't happened to
                                  // me, although I may have met ...).
                                  // This can't competently be a mistake;
                                  // perhaps the maintanance programmer
                                  // simply isn't cognizant of it.
                                  // But it is a little tricky, though.

CLASS CODE CHANGE: I say spirits can't turn into girls anymore, but can turn
  into boys.

                       person
                        /    \
        boy(spirit) -- boy   girl

Now, it is possible this change won't be known by the maintainer of the
above call to takesToDinner (or the call will be overlooked), although
good tools and standards would immediately point out the change.  But calls
to the original conversion constructor must be intentional (there really
isn't any such thing as unknown classes when providing arguments, except
for blunders), and it should also be obvious what was intended.  It is also
obvious that Freddy should now become a boy to attend dinner, an expected
change in the derived classes.


CASE 2) Another ctor is added along the same inheritance path.

       animal (Abstract Class)
         |
       person   (Abstract Class)
        /  \
      boy  girl -- girl(spirit&)
             \
          giftedGirl -- giftedGirl(spirit&);

Which *should* be called?
  1) The constructor closer to the formal's class (a closer D -> B match).
  2) The more specialized constructor.
  3) Neither, ambiguity error.

This seems a difficult question.  I first stated conversion functions and conv
by ctor were inversely symmetrical, but now I'm not sure.  Offhand, I'd guess
case 2) is best (the most specific match).

This example can also demonstrate case 1), the case of Freddy turning into
a killer dog to be taken to the park, which calls for an animal argument,
can only occur if the other constructors are removed simultaneously with
the addition of a giraffe conv ctor, which then obviously should occur, as
should the error that will correctly occur as Freddy can no longer go to
dinner.

Other issues are:
  1) Overloaded functions cause far greater code changes far more easily.
  2) Leaving out about half of all user-defined conversions in C++ seems
     excessive.  Is the cure worse than the disease?
  3) Explicit conversions can be called when desired.  This would eliminate
     any problems and cause an error if the constructor interface were
     changed (as does use of the new 'explicit' keyword.

And to stop playing Devil's advocate:
  1) Unintentional matching of an actual to a formal could occur on a
     constructor that was not intended for implicit conversion, but simply
     allows one argument for construction, or when that constructor wasn't
     intended.  This would be an argument passing blunder, but should the
     language enforce it not occuring?  It will happen anyway if the
     constructor is in the formal's class, perhaps with many default
     arguments.  But this is an obvious blunder and hasn't really been
     brought up.  This is the reason for code reviews.
  2) Too many conversions can lead to the Ada use-clause syndrom, but the
     new 'explicit' clause can help as would using explicit conversions.

Counter examples are most welcome (I am playing Devil's advocate), but I
think my time allotment on this issue is nearing an end.  I really just
wanted to have the standard defined, and luckily there is a clear and
recent paper on the subject (although I'm not sure about their stated
example or reasons).  And this has helped me to fully understand the issues
involved.  Perhaps now that I think I can relax with a standard definition,
it'll change again!

Thanks and Best Regards,
bob
rjh@geodesic.com





Author: Bob Hathaway <rjh@geodesic.com>
Date: 1995/04/19
Raw View
In article <AKV.95Apr11093358@srl03.cacs.usl.edu>,
Anil Vijendran <akv@cacs.usl.edu> wrote:
>
>>>>>> On Fri, 31 Mar 1995 19:22:50 GMT, Bob Hathaway <rjh@geodesic.com> said:
>
>    Bob> class A{};
>    Bob> class Q{};
>    Bob> class X: public Q{
>    Bob> public:
>    Bob>   X(const A&);
>    Bob> };
>
>    Bob> void fn(Q&);
>
>    Bob> int main(){
>    Bob>   A a;
>    Bob>   fn(a);  // Compilers report an error.
>    Bob>   return 0;
>    Bob> }
>
>This is how this should be handled:
>
>We need a type conversion from A to Q.
>
>Step 1. Look in the *scope* of A (the source type) if there are any
>user defined operator functions to convert to a Q or any class derived
>from Q.  There are none in our case.
>
>Step 2. Look (*only*) in the target type (Q) if there is a constructor
>that takes an A or a base class of A as parameter. The classes derived
>from Q are not searched for such constructors.
>...

I've just read the article on conversion functions and it appears the
case under question, searching derived classes for implicit conversion
by constructor was outlawed by the ANSI committee.  As another issue on
the construct validity of the decision,

They state:
  User code silently depending on conversions from classes completely
  unrelated to the classes they use.

???:
  I would consider derived classes relevant to the classes users use*,
  and had never considered slicing a hierarchy and only considering a
  part of it.  That is what browsers are for, displaying class
  hierarchies and perhaps implicit conversions.  And all changes should
  always be checked for any effects they may have (although this could
  be a problem in standardless settings).

 *In a polymorphic (and reuse) oriented environment, user code could be
  called with an object created out of their program's environment -
  this is the greatest strength of the OO approach and is relevant to
  overridable (virtual) functions, but not to statically typed conversion
  functions.  These can only be affected by source changes available to
  programmers and included in their programs; always available to
  programmers thru class browsers.


They state:
  If the user's code silenty depends [on unknown classes, MAJOR error]
  [...] changes to the interface of the conversion functions in the derived
  classes could then change the meaning of the user's code [...] unpleasant
  surprise for users.

???:
  I've already pointed out that such interface changes can't effect
  dependent user code and at worst (best?) would lead to ambiguity errors
  forcing the user to explicitly perform the conversion.  And my response
  above points out that unknown classes shouldn't exist.  And better,
  they mention a user depending on such a silent conversion of which they
  are not aware (but this is an error), and that a change to this stealth
  (which should never occur) conversion interface would change users
  code (simply incorrect in the direct case] - but so what if the user's
  defect is mildly changed; its still wrong, whether thru an incorrect
  constructor or the addition or change of several!   Admittedly, I am
  somewhat of an expert on this subject by trade, but this seems
  obvious to me.  If an argument is passed to a formal and requires any
  kind of conversion, the programmer should know what they are doing and
  be aware of any conversions that apply, a trivial check and the new
  'explicit' keyword could also help by forcing explicit conversion.  And
  obviously any change to a header file should be checked for code changes,
  standard SE practice.

  But wait, what about the case brought up by others, that another
  conversion in a derived class would be defined - what a fortuituos
  happening for our hypothetical problematical programmer - as an
  ambiguity error would now show up, revealing the original oversight, or
  even better, the original defect!!!  (Which again, should never have
  occurred.)  Is the committee being overprotective/restrictive in this
  case?  Perhaps with better standards and environments, could more
  flexibility and power be the greater guiding principle?

Best Regards,
bob
rjh@geodesic.com





Author: akv@srl03.cacs.usl.edu (Anil Vijendran)
Date: 1995/04/11
Raw View
>>>>> On Fri, 31 Mar 1995 19:22:50 GMT, Bob Hathaway <rjh@geodesic.com> said:

    Bob> class A{};
    Bob> class Q{};
    Bob> class X: public Q{
    Bob> public:
    Bob>   X(const A&);
    Bob> };

    Bob> void fn(Q&);

    Bob> int main(){
    Bob>   A a;
    Bob>   fn(a);  // Compilers report an error.
    Bob>   return 0;
    Bob> }

This is how this should be handled:

We need a type conversion from A to Q.

Step 1. Look in the *scope* of A (the source type) if there are any
user defined operator functions to convert to a Q or any class derived
from Q.  There are none in our case.

Step 2. Look (*only*) in the target type (Q) if there is a constructor
that takes an A or a base class of A as parameter. The classes derived
from Q are not searched for such constructors.

This is roughly what I learnt from a recent article in the C++ report
(mail me if you need more details on the article).

    Bob> In the call to fn above, shouldn't derived classes of Q be checked for
    Bob> conversion by constructor, inversely symmetrical with base classes of A

Which would you chose if there is another

 struct Y: Q
 {
   Y(const A&);
 };

which *I* wrote after you had `fn' up and running?

    Bob> I'm already aware of the ambiguity growth vs. expectancy vs. generality issues;
    Bob> again, I'd just like to know the official story.

The article was from someone in the ANSI/ISO group.

    Bob> Thank you in advance for any responses (directed to me or this newsgroup).

    Bob> Best Regards,
    Bob> bob
    Bob> rjh@geodesic.com

--
Anil

___________________________________________________________________________
Anil K Vijendran                    USL Box 43007, Lafayette, LA 70504-3007
akv@cacs.usl.edu                                         (318) 232-5502 [H]





Author: Bob Hathaway <rjh@geodesic.com>
Date: 1995/04/11
Raw View
In article <AKV.95Apr11093358@srl03.cacs.usl.edu>,
Anil Vijendran <akv@cacs.usl.edu> wrote:
>
>>>>>> On Fri, 31 Mar 1995 19:22:50 GMT, Bob Hathaway <rjh@geodesic.com> said:
>
>    Bob> class A{};
>    Bob> class Q{};
>    Bob> class X: public Q{
>    Bob> public:
>    Bob>   X(const A&);
>    Bob> };
>
>    Bob> void fn(Q&);
>
>    Bob> int main(){
>    Bob>   A a;
>    Bob>   fn(a);  // Compilers report an error.
>    Bob>   return 0;
>    Bob> }
>
>This is how this should be handled:
>
>We need a type conversion from A to Q.
>
>Step 1. Look in the *scope* of A (the source type) if there are any
>user defined operator functions to convert to a Q or any class derived
>from Q.  There are none in our case.
>
>Step 2. Look (*only*) in the target type (Q) if there is a constructor
>that takes an A or a base class of A as parameter. The classes derived
>from Q are not searched for such constructors.
>
>This is roughly what I learnt from a recent article in the C++ report
>(mail me if you need more details on the article).

I need more details; I haven't been keeping up with the C++ report.  And
thank you very much in advance for the reference, I'm looking forward to
reading it.  It does differ from the reference manuals if the standard
explicitly states derived classes of Q are not searched.

>
>    Bob> In the call to fn above, shouldn't derived classes of Q be checked for
>    Bob> conversion by constructor, inversely symmetrical with base classes of A
>
>Which would you chose if there is another
>
> struct Y: Q
> {
>   Y(const A&);
> };
>
>which *I* wrote after you had `fn' up and running?

This is an ambiguity error.  There is another thread in comp.lang.c++ (which
became separated from this one) in which this issue arose.  It's not a
problem because the conversion can be called explicitly after the compiler
reports an error (even if Y were made a Q with public inheritance).  As I
point out in the thread, the ellipses is the only conversion a user-defined
conversion will beat, a low level conversion which should always lose and
the rest should win over conversions or cause a trivially fixable ambiguity
errors.  Overloaded functions pose the only real problem here, calling for
standards.

>
>    Bob> I'm already aware of the ambiguity growth vs. expectancy vs. generality issues;
>    Bob> again, I'd just like to know the official story.
>
>The article was from someone in the ANSI/ISO group.

Great - this is what I was looking for.

Thanks and Best Regards,
bob
rjh@geodesic.com

P.S.  Is this a moderated newsgroup?  If not or if response times are very
  fast, I'll try to combine this thread with the one in comp.lang.c++.





Author: Bob Hathaway <rjh@geodesic.com>
Date: 1995/03/31
Raw View
class A{};

class Q{};
class X: public Q{
public:
  X(const A&);
};

void fn(Q&);

int main(){
  A a;
  fn(a);  // Compilers report an error.
  return 0;
}

In the call to fn above, shouldn't derived classes of Q be checked for
conversion by constructor, inversely symmetrical with base classes of A
being checked for conversion functions?  I don't see the non-inheritability
of constructors being relevant here (due to direction).  And shouldn't the
resultant type of either conversion be a (non-proper) derived class of the
formal, Q?  This conversion sequence would be 1 standard conversion and 1
user-defined conversion (or vice-versa), which doesn't violate any rule I'm
aware of.

Our current stock of C++ compiler's don't think so, but what is the standard?
I'm already aware of the ambiguity growth vs. expectancy vs. generality issues;
again, I'd just like to know the official story.

Thank you in advance for any responses (directed to me or this newsgroup).

Best Regards,
bob
rjh@geodesic.com