Currying/binding with ISO C99

北战南征 提交于 2019-12-12 15:58:35

问题


Say I want to implement a numerical integration routine with plain C. That will look something like this:

double integrate(double (*f)(double), double lower, double upper, double step));

I often find functions that actually depend on multiple variables, and I want to integrate over the first one. Say I want to integrate this:

double func(double x, double z);

with respect to x. I cannot pass func to integrate since it has the wrong signature. Now I know the following workarounds, which were employed by us when we took the numerics course:

  • Use C++

    I just have used C++ and ist std::bind to create a functor (function object) that I could pass to the integration routine. Now I would just use the lambda functions to get it done.

  • Use GCC extension for functions in function

    With GCC, you can declare a function in a function. So one could do

    // z is set to some value in this function scope here.
    double inner(double x) {
        return func(x, z);
    }
    

    and pass that inner to the integrate function. That is non-standard and does not feel so well.

  • Use global variables

    The value of z could be stored in a global variable. That would require the function func to be editable to use z from the global variables instead of the parameter. That might not be possible. Then it also breaks concurrency and is just bad in general.

Does a way exist to do with in plain C without breaking something?


回答1:


One common solution to this problem is to change the design into this:

double integrate(double (*f)(double, void*), void*,
                      double lower, double upper, double step);

Here you pass an additional void * to integrate, and this is being passed back to f. This can be used to pass arbitrary data around, in your case you would pass a pointer to z, and within the function f you would cast the pointer back to its original type and recover the value z. This pattern is ubiquitous within C libraries, for example here is the prototype of pthread_create:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                      void *(*start_routine) (void *), void *arg);



回答2:


No, C has no way to do this at the level of C functions/C function pointers. The best way to do it for a particular application (like mathematical functions and integration) would be to use your own structures to represent mathematical functions, and any variable slots that are already bound. I would probably not make the functions take varying numbers of arguments, but instead a pointer to an array of arguments; this makes it a lot easier to call them programmatically in different ways.

Alternatively you could use something like libffi which can do this kind of binding/closures, but it's definitely not portable or efficient.




回答3:


Your problem can also be solved with variable arguments functions (and slightly more effort):

#include <stdarg.h>

double integrate(double (*f)(double, ...), double lower, double upper, double step)
{
    return (f)(lower, upper);
}

double func1(double x, ...)
{
    va_list ap;
    double ret;

    va_start(ap, x);

    ret = x * va_arg(ap, double);

    va_end(ap);

    return ret;
}

double func2(double x, ...)
{
    return x;
}

Although not sure if I should consider that in any way cleaner...




回答4:


I think your example under Use GCC extension for functions in function is a viable solution. And incidentally, it is not an example of a GCC extension, but simply a helper function.

double inner(double x) {
    return func(x, 100);
}

That's perfectly valid C89.




回答5:


I'm 99% sure it's not possible with pure C89. In order to do this, you have to create a new function pointer at runtime. There are two ways to get a function pointer: from a function expression, or from a standard library function that returns a function pointer. Function expressions refer to functions defined at compile time, so that won't work. The only standard library function that returns a function pointer is signal, and that's no help because you only get out of it what you put in.

The only other way to get a new function pointer would be to convert a pointer to an object type into a function pointer, and that's not portable because it's not in the list of pointer conversions that you can do (however, it is noted as a common extension).

For a little while I thought you might be able to get somewhere with setjmp and longjmp, but that just replaces the problem of storing the double with the problem of storing the jmp_buf.

I did get something that happens to work on my system today, but since it's nonportable even upgrading my compiler might break it. The general idea is to create a structure that contains the pointer to the original function, the double z, and some machine code to access that information and call the original. I don't suggest you use this, but I found it interesting. I've pointed out some of the unportable assumptions in the comments.

/* platform-specific include */
#include <sys/mman.h>
/* standard includes */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

double func(double x, double z)
{
    printf("%g\n", z);
    return x;
}

double bar(double x)
{
    printf("y\n");
    return x;
}

double call(double (*f)(double))
{
    return f(0.0);
}

struct cDblDblRetDbl
{
    double (*function)(double, double);
    double a;
    char code[1];
    double pad;
};

static double helper(double x)
{
    /* Casting a function pointer to an object pointer is
     * not provided by the standard.
     * In addition, this only works because the compiler
     * happens to use RIP-relative addressing, so "helper"
     * points to the beginning of the currently executing
     * function, which is actually a copy of the one in
     * the object file.
     * It's worth noting explicitly that nothing in the
     * C standard says that a pointer to a function will
     * point to its machine code.
     */
    double *dp = (double *) helper;
    struct cDblDblRetDbl *data;
    /* Modify it to point after the structure.
     * This depends on the alignment and padding of the
     * structure, which is not portable.
     */
    dp += 2;
    data = (struct cDblDblRetDbl *) dp;
    /* back it up to the structure */
    --data;
    /* now call the function with the saved data. */
    return data->function(x, data->a);
}

/* There is no way to get the code size of a function,
 * so this is very nonportable.
 * I found it by examining the object file.
 */
#define CODE_SIZE 0x60

double (*curryDoubleDoubleReturningDouble(double (*function)(double, double), double a))(double)
{
    size_t size = sizeof(struct cDblDblRetDbl) + CODE_SIZE;
    /* valloc is nonstandard but we need an area aligned to a
     * page boundary for mprotect.
     */
    struct cDblDblRetDbl *result = valloc(size);
    result->function = function;
    result->a = a;
    /* Copy the code of the helper function into the structure.
     * Once again, we're casting a function pointer to an
     * object pointer and the standard doesn't say you can do that. 
     */
    memcpy(result->code, (void *) helper, CODE_SIZE);
    /* Memory protection is out of the scope of the standard,
     * and in a real program we need to check the return value.
     */
    mprotect(result, CODE_SIZE, PROT_READ | PROT_EXEC | PROT_WRITE);
    /* Casting an object pointer to a function pointer is also
     * not provided by the standard.
     * This example leaks memory.
     */
    return (double(*)(double)) result->code;
}

int main()
{
    call(bar);
    call(curryDoubleDoubleReturningDouble(func, 5));
    call(curryDoubleDoubleReturningDouble(func, 7));
    call(curryDoubleDoubleReturningDouble(func, 42.9));
}

If you wrote helper in assembly and created OS-specific versions of curryDoubleDoubleReturningDouble, you could probably get this working a lot of places. But I'm sure there are some computers C runs on where you can't do this.



来源:https://stackoverflow.com/questions/23849343/currying-binding-with-iso-c99

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!