Function call and type promotion

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • Yannick

    Function call and type promotion

    Hi everyone -

    I am not quite sure to understand what is really going on when a
    function defined in one translation unit calls a function defined in a
    different translation unit without knowing its prototype. Let's say for
    instance :

    foo.c

    #include <stdio.h>

    void foo(float a, int b)
    {
    printf("%f (0x%x) %d (0x%x)\n", a, &a, b, &b);
    }



    main.c

    int main(void)
    {
    float a = 5.0;
    int b = 10;

    foo(a, b);

    return 0;
    }

    According to what I have read, the argument a should be promoted to
    double, but the function foo expects to get a float as its first
    parameter. The result is undefined as the binary representation / space
    occupied by a float and a double are completely different. I'm OK with
    that.

    However, if I change the foo function definition to foo(char a, char b)
    the arguments should be promoted to int. As foo would expect two char
    as parameters the address of a and b should be *(ebp + 4) and *(ebp +
    5). This is not the case, it appears that b is located sizeof(int)
    deeper in the stack than a so the function displays the correct values.

    I do not understand why the behavior is different ... did I miss
    something related to type promotion and the circumstances under which
    it occurs?


    Thank you,
    Yannick

  • Eric Sosman

    #2
    Re: Function call and type promotion

    Yannick wrote:
    Hi everyone -
    >
    I am not quite sure to understand what is really going on when a
    function defined in one translation unit calls a function defined in a
    different translation unit without knowing its prototype. Let's say for
    instance :
    >
    foo.c
    >
    #include <stdio.h>
    >
    void foo(float a, int b)
    {
    printf("%f (0x%x) %d (0x%x)\n", a, &a, b, &b);
    }
    >
    >
    >
    main.c
    >
    int main(void)
    {
    float a = 5.0;
    int b = 10;
    >
    foo(a, b);
    >
    return 0;
    }
    >
    According to what I have read, the argument a should be promoted to
    double, but the function foo expects to get a float as its first
    parameter. The result is undefined as the binary representation / space
    occupied by a float and a double are completely different. I'm OK with
    that.
    >
    However, if I change the foo function definition to foo(char a, char b)
    the arguments should be promoted to int. As foo would expect two char as
    parameters the address of a and b should be *(ebp + 4) and *(ebp + 5).
    This is not the case, it appears that b is located sizeof(int) deeper in
    the stack than a so the function displays the correct values.
    >
    I do not understand why the behavior is different ... did I miss
    something related to type promotion and the circumstances under which it
    occurs?
    The only thing you've missed is that "undefined behavior"
    does not guarantee some kind of run-time error, and might even
    match what somebody was hoping for.

    No function with a "promotable " parameter[*] can be called
    correctly without having a prototype in scope at the point of
    the call, for exactly the reason you've described: If there's
    no prototype, the caller promotes the arguments and this means
    they do not match the actual types the function expects. What
    happens next is undefined: The program might abort, it might
    run but give weird results, it might make vanilla pudding ooze
    from your keyboard -- or it might "work." If it works, though,
    it's because of luck (good? bad?) and not because of skill.
    [*] Note that "K&R-style" functions never have promotable
    parameters, even if they seem to be written that way:

    void foo(f)
    float f;
    { ... }

    takes a non-promotable `double' parameter which is then demoted
    to `float'. The function is roughly equivalent to

    void foo(double _fake_f) {
    float f = _fake_f;
    ...
    }

    --
    Eric.Sosman@sun .com

    Comment

    • Keith Thompson

      #3
      Re: Function call and type promotion

      Yannick <ydaffaud@hotma il.comwrites:
      I am not quite sure to understand what is really going on when a
      function defined in one translation unit calls a function defined in a
      different translation unit without knowing its prototype. Let's say
      for instance :
      >
      foo.c
      >
      #include <stdio.h>
      >
      void foo(float a, int b)
      {
      printf("%f (0x%x) %d (0x%x)\n", a, &a, b, &b);
      }
      >
      >
      >
      main.c
      >
      int main(void)
      {
      float a = 5.0;
      int b = 10;
      >
      foo(a, b);
      >
      return 0;
      }
      >
      According to what I have read, the argument a should be promoted to
      double, but the function foo expects to get a float as its first
      parameter. The result is undefined as the binary representation /
      space occupied by a float and a double are completely different. I'm
      OK with that.
      Right, except that the binary representations *may or may not* be
      completely different. (I've seen systems where a double is simply a
      float with extra mantissa bits added at the end; on such a system,
      with the right parameter passing conventions, a certain byte ordering,
      and a strong tail-wind, it just might "work". Such is the nature of
      undefined behavior.)
      However, if I change the foo function definition to foo(char a, char
      b) the arguments should be promoted to int.
      Not quite. Remember, when the compiler is handing the call to foo, it
      can't see the definition of foo, so nothing in the definition of foo
      can affect how the arguments are promoted.

      (First, an important piece of terminology: A *parameter* is an object,
      local to a function, defined and declared between the parentheses in
      the prototype. An *argument* is an expression passed to a function,
      appearing between the parentheses in a function call. In the
      evaluation of a function call, each argument is evaluated, and the
      resulting value is assigned to the corresponding parameter.)

      If there's no prototype determining the parameter type (i.e., either
      there's no prototype at all or the argument corresponds to the "..."
      in a variadic function), float is promoted to double. If foo happens
      to be expecting a double argument, you're ok; if not, you're in the
      land of undefined behavior.

      If you passed a char *argument*, it would be promoted to int (or
      possibly to unsigned int given sufficiently exotic system
      characteristics ). And you'd be ok if the function expected an int; if
      it doesn't, you have UB.
      As foo would expect two
      char as parameters the address of a and b should be *(ebp + 4) and
      *(ebp + 5). This is not the case, it appears that b is located
      sizeof(int) deeper in the stack than a so the function displays the
      correct values.
      The *parameters* are of types float and int, respectively, because you
      defined them that way. If you examine their addresses within the body
      of foo, you're looking at where foo *expects* them to be. But since
      you've already invoked undefined behavior, you'll *probably* get the
      right addresses, but there are no guarantees.
      I do not understand why the behavior is different ... did I miss
      something related to type promotion and the circumstances under which
      it occurs?
      The point is that the behavior *isn't* different. The addresses of
      the parameters shouldn't be affected by what was passed as arguments
      (though their values certainly will be).


      --
      Keith Thompson (The_Other_Keit h) kst-u@mib.org <http://www.ghoti.net/~kst>
      Nokia
      "We must do something. This is something. Therefore, we must do this."
      -- Antony Jay and Jonathan Lynn, "Yes Minister"

      Comment

      • Yannick

        #4
        Re: Function call and type promotion

        Understood. Thanks for the help!


        Yannick

        Comment

        • CBFalconer

          #5
          Re: Function call and type promotion

          Yannick wrote:
          >
          I am not quite sure to understand what is really going on when a
          function defined in one translation unit calls a function defined
          in a different translation unit without knowing its prototype.
          Let's say for instance :
          >
          foo.c
          >
          #include <stdio.h>
          >
          void foo(float a, int b) {
          printf("%f (0x%x) %d (0x%x)\n", a, &a, b, &b);
          }
          You failed to #include "foo.h", which should read:

          void foo(fload a, int b);
          >
          main.c
          >
          int main(void) {
          float a = 5.0;
          int b = 10;
          >
          foo(a, b);
          return 0;
          }
          And here you also failed to #include "foo.h". The purpose of
          header files is to make characteristics of cfiles available to
          other cfiles, so that they can be correctly used.

          .... snip musing on undefined behavior ...

          --
          [mail]: Chuck F (cbfalconer at maineline dot net)
          [page]: <http://cbfalconer.home .att.net>
          Try the download section.


          ** Posted from http://www.teranews.com **

          Comment

          • Yannick

            #6
            Re: Function call and type promotion

            On 2008-05-21 21:51:32 +0200, CBFalconer <cbfalconer@yah oo.comsaid:
            And here you also failed to #include "foo.h". The purpose of
            header files is to make characteristics of cfiles available to
            other cfiles, so that they can be correctly used.
            [..] what is really going on when a function defined in one translation
            unit calls a function defined in a different translation unit without
            knowing its prototype. [...]

            That's why I intently didn't use a header file for this one. I didn't
            want the compiler to know about the function prototype.

            I know one should always provide the compiler with functions prototypes
            but that's not the point of this discussion.


            Yannick

            Comment

            • Chris Torek

              #7
              Re: Function call and type promotion

              In article <20080521191350 16807-ydaffaud@hotmai lcom>
              Yannick <ydaffaud@hotma il.comwrote:
              >I am not quite sure to understand what is really going on when a
              >function defined in one translation unit calls a function defined in a
              >different translation unit without knowing its prototype.
              Depending on what the correct prototype is, this *can* even be
              guaranteed to work. It may well work despite a lack of (Standard)
              guarantee, however (as others have described else-thread).
              >Let's say for instance :
              [snippage; vertical space editing]
              >void foo(float a, int b) { printf("%f (0x%x) %d (0x%x)\n", a, &a, b, &b); }
              ...
              >However, if I change the foo function definition to foo(char a, char b) ...
              >the address of a and b should be *(ebp + 4) and *(ebp + 5).
              Given that you mention "ebp", we can guess (without any absolute
              guarantee of being right, especially someday in the future when
              some other manufacturer uses that name for something entirely
              different) that you refer to the x86 architecture, and one of
              the common C compilers for that architecture.
              >This is not the case, it appears that b is located sizeof(int)
              >deeper in the stack than a so the function displays the correct values.
              Most x86 compilers, most of the time, under mostly-default conditions
              -- and all these "most"s are in fact important -- "just happen"
              (for a number of historical reasons) to pass "float" parameters as
              "double"s on the hardware-provided stack addressed via %esp (I use
              this wording to distinguish it from the FPU stack). They pass
              "char" parameters only after widening them to "int", again on the
              hardware-provided stack addressed via %esp.

              Most of these compilers (under said conditions) do this whether or
              not there is a prototype present at the site of the call, and hence
              produce runtime code for foo() that assumes that the "float"
              parameter was widened to "double", and that the "char" parameters
              were widened to "int".

              C compilers do not have to do this, and if they choose not to be
              compatible with "most" x86 C compilers, they *can* use some sort
              of "better" parameter-passing mechanism(s). Many C compilers can
              even be told to use these "better" mechanisms even while maintaining
              some compatibility, either on a function-by-function basis (using
              #pragma or __attribute__ or __fastcall or __other_magic_w ord, or
              perhaps on a more global basis, by something like -mregparm=N).
              The "better" conventions *can* be the *default*: since Standard C
              says that the C programmer, not the implementation, is in the wrong
              for failing to provide proper prototypes, Standard C allows the
              code to fail in the absence of proper prototypes. It is merely
              the (strong) draw of compatibility (and/or convenience) that keeps
              C compiler vendors using the "worse" calling conventions that make
              poorly-written, non-Standard code "work" anyway.

              ("Better" is in quotes since "better"-ness is necessarily somewhat
              an "eye of the beholder" thing. The "best" way to do something is
              a lot like the "best" flavor of ice cream.)
              --
              In-Real-Life: Chris Torek, Wind River Systems
              Salt Lake City, UT, USA (40°39.22'N, 111°50.29'W) +1 801 277 2603
              email: gmail (figure it out) http://web.torek.net/torek/index.html

              Comment

              • CBFalconer

                #8
                Re: Function call and type promotion

                Yannick wrote:
                CBFalconer <cbfalconer@yah oo.comsaid:
                >
                >And here you also failed to #include "foo.h". The purpose of
                >header files is to make characteristics of cfiles available to
                >other cfiles, so that they can be correctly used.
                >
                [..] what is really going on when a function defined in one
                translation unit calls a function defined in a different
                translation unit without knowing its prototype. [...]
                >
                That's why I intently didn't use a header file for this one. I
                didn't want the compiler to know about the function prototype.
                >
                I know one should always provide the compiler with functions
                prototypes but that's not the point of this discussion.
                Undefined behavious is, well, undefined. There is no discussion.

                --
                [mail]: Chuck F (cbfalconer at maineline dot net)
                [page]: <http://cbfalconer.home .att.net>
                Try the download section.


                ** Posted from http://www.teranews.com **

                Comment

                • Keith Thompson

                  #9
                  Re: Function call and type promotion

                  CBFalconer <cbfalconer@yah oo.comwrites:
                  Yannick wrote:
                  >CBFalconer <cbfalconer@yah oo.comsaid:
                  >>
                  >>And here you also failed to #include "foo.h". The purpose of
                  >>header files is to make characteristics of cfiles available to
                  >>other cfiles, so that they can be correctly used.
                  >>
                  >[..] what is really going on when a function defined in one
                  >translation unit calls a function defined in a different
                  >translation unit without knowing its prototype. [...]
                  >>
                  >That's why I intently didn't use a header file for this one. I
                  >didn't want the compiler to know about the function prototype.
                  >>
                  >I know one should always provide the compiler with functions
                  >prototypes but that's not the point of this discussion.
                  >
                  Undefined behavious is, well, undefined. There is no discussion.
                  There was a discussion, and it was fairly interesting. Perhaps you
                  missed it. It included some good points about why the behavior is
                  undefined, as well as circumstances when it isn't undefined (when the
                  promoted argument types match the declared parameter types). And,
                  yes, it also included some harmless digressions into the actual
                  behavior a system is likely to exhibit in the presence of this
                  undefined behavior; this was relevant to the reasons that the standard
                  defines (or undefines) things the way it does, and to the practice of
                  diagnosing problems caused by undefined behavior.

                  --
                  Keith Thompson (The_Other_Keit h) kst-u@mib.org <http://www.ghoti.net/~kst>
                  Nokia
                  "We must do something. This is something. Therefore, we must do this."
                  -- Antony Jay and Jonathan Lynn, "Yes Minister"

                  Comment

                  • Peter Nilsson

                    #10
                    Re: Function call and type promotion

                    Keith Thompson <ks...@mib.orgw rote:
                    CBFalconer <cbfalco...@yah oo.comwrites:
                    Yannick wrote:
                    I know one should always provide the compiler
                    with functions prototypes but that's not the
                    point of this discussion.
                    Undefined behavious is, well, undefined.  There
                    is no discussion.
                    >
                    There was a discussion, and it was fairly interesting.
                    Perhaps you missed it.  It included some good points
                    about why the behavior is undefined, as well as
                    circumstances when it isn't undefined (when the
                    promoted argument types match the declared parameter
                    types).  And, yes, it also included some harmless
                    digressions into the actual behavior a system is
                    likely to exhibit in the presence of this
                    undefined behavior;
                    IMHO, the last bit is not harmless.
                    this was relevant to the reasons that the standard
                    defines (or undefines) things the way it does, and
                    to the practice of diagnosing problems caused by
                    undefined behavior.
                    Unfortunately, many newbies will focus exclusively
                    on the behaviour of undefined behaviour and in some
                    cases end up convincing themselves that there is no
                    other possible behaviour from any other system.
                    Worst case scenario, they will use the construct
                    and conclude that any system that doesn't exhibit
                    the desired behaviour is flawed and not worth
                    considering as a target platform.

                    In this case, the problem of unprotoyped functions
                    is so old and so well known, very few compilers
                    will not provide a facility for at least warning
                    of unprototyped function use.

                    Prevention is better than cure. C99 requires at
                    least a declaration, but I prefer to break
                    conformance and, were possible, force the compiler
                    to error out if I use an unprototyped function.

                    --
                    Peter

                    Comment

                    Working...