←back to thread

218 points signa11 | 1 comments | | HN request time: 0s | source
Show context
throwaway7894 ◴[] No.43681266[source]
As someone who has a file with similar hacks, I will say this: I am not a C++ fan, but if you find yourself writing C code where you simulate methods via structs with function pointers often, just use C++ as a basic "C with classes" at that point. You want methods anyway, you have to go through a pointer dereference to call the function, it's just not worth the code weirdness. If you have the grit to use structs with function pointers everywhere, you have the grit to stick to the simpler subset of C++.
replies(5): >>43683169 #>>43683849 #>>43684044 #>>43701516 #>>43703558 #
unclad5968 ◴[] No.43683849[source]
I'm torn. The step from C to any c++ is big. Now if you want anybody to be able to use your code they need to be using c++ or you have to provide a C api anyway. On the other hand, manually implementing vtables is annoying. Ive been sticking to pure C and haven't been bothered enough to go back to any c++ yet (about 6 months on my current project). I mostly only miss templated containers so far.
replies(1): >>43701734 #
ryao ◴[] No.43701734[source]
It is more annoying to want to implement an optional function in a class and then have no simple way to check if that optional function is implemented in the object without, having to edit code that guards the call sites every time you add a derived class that implements it, or having to implement your own way of querying the object to know if it is supported.
replies(2): >>43701974 #>>43702572 #
unclad5968 ◴[] No.43701974[source]
I've never come across a situation where I wanted to do this. What would be a use case for optional class functions?
replies(1): >>43702373 #
ryao ◴[] No.43702373[source]
Well, it would not be a class in that case, but a use case would be anything that is modular where you want to support a new function, but do not require all of your implementors to use it. The best example of this is the VFS. Here is documentation for the Linux VFS:

https://www.kernel.org/doc/html/latest/filesystems/vfs.html

The vast majority of the function pointers in those structures are optional (even if not explicitly stated). To give a few common sense examples:

  * If your filesystem does not support extended attributes, you would not implement .listxattr and instead set it to NULL.
  * There are multiple ways of implementing read and write in file_operations. You have the basic read and write operations, and more efficient variants. You don’t need to implement the more efficient variants if you don’t want to implement them.
  * The .bmap call is used to find out how the filesystem stores a file on a block device, which used to be used by the syslinux (and might still be). This obviously is incompatible with NFS (or any multidisk filesystem like ZFS) so it absolutely must be optional.
Then there are other options, like not supporting mmap, or not supporting creation/removal of subdirectories. That sounds absurd, but some FUSE filesystems, particularly those exporting a program’s statistics, don’t bother with either of those since they are not needed. I do not believe Linux sysfs allows users to make directories either.

I could continue, but this gives a few examples of why you might want to have optional functionality in a class-like interface.

By the way, I mentioned setting things you do not implement to NULL. This is done simply by not specifying them when using the structure initializer syntax. The compiler will zero unspecified members.

replies(1): >>43703787 #
pjmlp ◴[] No.43703787[source]
Nothing prevents implement something like that in C++, it is still C++ code.

If you want to make something fancy, templates, if constexpr requires func-to-call, call func.

replies(3): >>43704295 #>>43704296 #>>43704299 #
ryao ◴[] No.43704299[source]
The point of the throwaway account’s comment was to say that you should use C++ class member functions instead of C function pointers in structures, but that is impossible to do in the general case in a sane way, since there is no way to leave a member function unimplemented and then check its status at runtime.

You need to use hacks to shoehorn C++ class member functions into this. In particular, you need stub functions. Then either, call them and have them either return a special error code or throw an exception, or use a custom query function that is implemented by derived classes that lets you find out if a function is a stub or not to allow you to skip calling it. Another idea would be to use thread local storage with setjmp()/longjmp(), which is probably the sanest way of doing this insane idea:

https://godbolt.org/z/4GWdvsz6z

And the C way for comparison:

https://godbolt.org/z/qG3v5zcYc

The idea that the simplest way of approximating what you can do with function pointers in C structures via C++ class member functions is to use TLS and setjmp/longjmp shows what a bad idea it is to use class member functions instead of function pointers for optional functions in the first place.

replies(1): >>43706439 #
pjmlp ◴[] No.43706439[source]
C++ was designed as "Typescript for C" for its time, because sometimes that is exactly the kind of code one needs to write, even if we discourage many of the classical patterns when better alternatives exist.

The same C example compiled in C++23 mode, https://godbolt.org/z/MWa7qqrK7

As for possible alternatives, here is a basic one without taking into consideration virtual mechanics, only to show the principles.

    #include <concepts>

    template <class T>
    concept has_mmap = requires (T obj)
    {
        { obj.mmap() } -> std::convertible_to<int>;

    };

    class VFS {
    public:
        VFS() = default;
        virtual ~VFS() = default;
    };

    class ExampleFS : public VFS {
        // mmap not available
    };

    class ExampleWithMMAP : public VFS {
    public:

        int mmap() {
            return 0;
        } 
    };

    int main() {
        ExampleFS fs;
        ExampleWithMMAP fsWithMMAP;

        /*
        <source>: In function 'int main()':
        <source>:33:19: error: 'class ExampleFS' has no member named 'mmap'
         40 |         return fs.mmap();
            |    
        */
        if constexpr (has_mmap<ExampleFS>) {
            return fs.mmap();
        }

        // ExampleWithMMAP has mmap(), just call it without issues
        if constexpr (has_mmap<ExampleWithMMAP>) {
            return fsWithMMAP.mmap();
        }

        // want to use the variable name instead of the type?
        if constexpr (has_mmap<decltype(fsWithMMAP)>) {
            return fsWithMMAP.mmap();
        }
    }
-- https://godbolt.org/z/cjcbrzT3z

Naturally it is possible to be a bit even more creative, and moreso with C++26 reflection.

replies(1): >>43721045 #
1. ryao ◴[] No.43721045{3}[source]

  The same C example compiled in C++23 mode, https://godbolt.org/z/MWa7qqrK7
Everyone knows this. The original comment was saying not to do this (even in C++) and use C++ classes instead. I was making the point that is a bad idea. You seem to have not understood that.