Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

List size is O(n).

https://eastl.docsforge.com/master/design/#listsize-is-on

I am surprised compilers don't magically optimize the code and remove the size-remembering variable if it is not needed.



> I am surprised compilers don't magically optimize the code and remove the size-remembering variable if it is not needed.

the problem is that technically anyone can come and do

    auto lib = dlopen("my_lib.so", 0);
    auto sym = dlsym(lib, "_ZNKSt7__cxx114listIiSaIiEE4sizeEv"); // std::list<int>::size()
    auto func = static_cast<void(*)(std::list<int>*)>(sym);
    std::list<int> some_local_list = ...;
    (*func)(some_local_list);
and that is expected to not crash - template symbols are generally part of your shared library API (at least that's been the default, however bad it is, on Linux, for a very long time). If compilers were optimizing the layout of individual list instances, then the above wouldn't work anymore (unless the compiler would inline / create new symbols for each individual cases in your code which would make the object sizes go through the roof).


There's no requirement or even expectation that your snippet of code in principle works (even after working out the issues in the snippet you posted, such as needing to use the .* operator, the fact that member functions are not the same size as void*, and other quirky details).

The only way something somewhat similar to what you posted could possibly work would be to have some_local_list allocated/constructed dynamically from the same shared object that contains the the member function and to expose the member function using extern "C".

For example something like:

    auto some_local_list = create_list(...);
    list_size(some_local_list);
Where create_list is a function loaded from the same dlopen and returns a dynamically allocated and opaque handle to a list, and list_size is also a function loaded from dlopen that is exported using extern "C".

With this approach it's certainly possible for a compiler to optimize out unused member variables. Any other approach is undefined behavior and may or may not work.


> There's no requirement or even expectation that your snippet of code in principle works

"in principle" goes away as soon as we use dlopen, as it implies a lot of things on the way C++ will be supported on that given platform ; no one cares about C++ in a vacuum.

In practice, different .dll / .so / .dylib communicate through C++ APIs all the time ; all relevant platforms have to support that at some level (which can sometimes cause strong headaches, for sure: https://www.codesynthesis.com/~boris/blog/2010/01/18/dll-exp... ).


When I say in principle, I don't mean according to a strict interpretation of the standard in a vacuum. I mean that even if I were to extend to you a great deal of liberties and operate at the level of what you're attempting to accomplish, your approach is fundamentally invalid and results in buggy code that will break and is entirely unneccessary.

The way you accomplish dynamically loading member functions is by exporting a plain C function using extern "C" that takes an opaque handle to the object you wish to operate on, and whose implementation wraps the member function whose operation you wish to expose.

Pointers to member functions are fundamentally not compatible with void* and hence may not reliably be returned using dlsym. Only once the member function is bound to an object (using the .* operator, ie. object.*member) is the resulting pointer compatible with a void* (in C++11 it's implementation defined). Until then, they not only have different sizes, their size may even be different within different translation units of the same application!

In practice you are right that DLLs and shared objects communicate through C++ APIs all the time, and the reliable way that they do so is by using extern "C". The article you linked to is exactly the kind of pain, undefined behavior, and buggy problems you will encounter when you try to use any other mechanism than the plain and straight forward mechanism that exists precisely for the purpose of facilitating this kind of communication.

The reason my point is worth making, as opposed to just being a pedantic technicality, is because this approach is precisely what allows compilers to make various optimizations that continue to work safely even in situations where objects, functions, and member functions are used across dynamic boundaries. If you don't follow this approach, then the compiler will make certain optimizations that will result in disastrous behavior.

If you don't want to take my word for it, hopefully you'll take the advice of the ISO CPP [1]:

"do not attempt to “cast” a pointer-to-member-function into a pointer-to-function; the result is undefined and probably disastrous. E.g., a pointer-to-member-function is not required to contain the machine address of the appropriate function."

[1] https://isocpp.org/wiki/faq/pointers-to-members#addr-of-memf...


I have seen implementations of type erasure/delegates that store an instance pointer and pointer-to-member-function as an instance and pointer-to-member-function of a dummy type with the same calling convention. It's interesting how many different member function pointer sizes Windows has. Undefined behavior and compiler dependent, but consistent.


Or COM like APIs.


These sort of layout optimizations are extremely hard in C-like languages as the compiler needs to prove that the program can't tell the difference. At the very least you need whole program optimizations.


Having compilers remove members would be a backwards compatibility shitshow.


Isn't space complexity for a list with cached count (size) property "c" still O(n + c) ~ O(n)?


However list splice is O(1), since it doesn't have to count how many items it moves.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: