-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More C++y API #22
Comments
The C++ wrapper is meant to confer RAII semantics rather than using specific C++ types (e.g. I would prefer not to use
I would prefer not to use I'm opposed to replacing the current C++ API by your proposal, but am neutral to adding a separate header file (e.g. |
I got the impression that this project uses C++17. The following line should probably be edited in that case: nativefiledialog-extended/CMakeLists.txt Line 24 in d1b8b19
|
I think It's unlikely to be a performance bottleneck in any case, but it would make this library opinionated (in the sense that it forces users to convert to/from |
Checking the CMake docs, you're right. I don't think it would be more opinionated than using a I think adding another C++ API on top of the existing 2 would risk making stuff painful to maintain, |
I'm not too sure what you mean - it appears that CMAKE_CXX_STANDARD sets the default value of CXX_STANDARD on all targets, which is what we want here. This option will be set on NFDe only, and not your project that uses this library, according to this StackOverflow answer.
Yeah that's right. But I believe the CMAKE_CXX_STANDARD option that we set only applies to the code that is actually compiled into a static library, which excludes that
It would be the default in most small applications, but there are still many situations where the default string representation isn't
I'm not fully sure if I agree or disagree with this, so I'm currently neutral to having a second C++ API. But I still do not think that replacing the current API is a good thing to do.
Perhaps you could consider git submodules if you would like to get future bug fixes, like what this repo does. |
Nice, now I know :) Good idea about submodules, may try that. |
I implemented a basic wrapper around the API in my project using nfde. Thought I'd show you how it looks/what I had in mind: namespace util::nfd {
namespace fs = std::filesystem;
using string = fs::path::string_type;
struct FilterItem {
string niceName;
string suffixes;
};
[[nodiscard]] auto
saveDialog(
std::vector<FilterItem> const& filterItems,
fs::path const& startPath,
string const& defaultName) -> std::pair<fs::path, nfdresult_t>;
[[nodiscard]] auto
openDialog(
std::vector<FilterItem> const& filterItems,
fs::path const& startPath) -> std::pair<fs::path, nfdresult_t>;
[[nodiscard]] auto
openDialogMultiple(
std::vector<FilterItem> const& filterItems,
fs::path const& startPath)
-> std::pair<std::vector<fs::path>, nfdresult_t>;
} // namespace util::nfd
auto
operator""_nfd(char const* str, size_t size)
-> std::filesystem::path::string_type
{
return {str, str + size};
} Used like this: auto
loadTerrain(
Terrain& terrain,
ShaderController& shaderController,
ShaderProgram& shaderProgram) -> void
{
namespace fs = std::filesystem;
std::vector<util::nfd::FilterItem> filterItems{
{"Terrain files"_nfd, "lua,frag"_nfd}};
auto const [paths, result] = util::nfd::openDialogMultiple(
filterItems,
fs::current_path() / "presets");
if(result != NFD_OKAY) {
return;
}
for(auto const& path : paths) {
if(util::endsWith(path.native(), "shape.lua"_nfd)) {
std::ifstream in(path);
terrain.loadLua(util::getContents(in));
}
// ...more code...
}
} |
Sorry, I was quite busy for the past few days. Indeed, this API looks quite concise, and it is nice that I'm also wondering if the vector should be changed to a pair of iterators instead, so that it can also accept other iterable types (this won't add any overhead since in all cases we need to build the array of template <typename InIt>
[[nodiscard]] auto
saveDialog(
InIt filterItems_begin,
InIt filterItems_end,
fs::path const& startPath,
string const& defaultName) -> std::pair<fs::path, nfdresult_t>; |
We should probably then teach the simpler API first, and refer to other documentation for those who
I agree that we should be agnostic about the input container. template<class Container>
[[nodiscard]] auto
saveDialog(Container const& filterItems, ...) -> std::pair<fs::path, nfdresult_t>;
{
// use std::cbegin(filterItems), std::cend(filterItems)
} This would eventually be pretty nice when C++20 comes around to be viable, Should be possible to emulate that concept already using some SFINAE magic... |
Currently the documentation (i.e. contract) for the API is in comments in the header file itself. I would think that is reasonable, though it might be useful to have an additional page of documentation (in markdown) for each header file.
I'm not really sure if templating on the input container would be good, since sub-slices cannot be represented by a standard container type until C++20. The pair of iterators seem to be more "idiomatic" C++, even in C++20 unless the user is manipulating ranges. I'm not too keen on trying to emulate concepts here though, since that may confuse people who try to read the header file. In any case, in C++<20, we would just let the compiler produce long-ish error messages when instantiating templates with incorrect types, so there might not be reason to deviate from the "standard" way of doing things. Just saying that |
I don't think I can think of a situation where you'd want a sub-slice of a collection here. |
Maybe the user wants to display a prefix of some list of filters, depending on the game settings?
Yeah that's possible, by then we would have more clarity on whether using |
The main reason to deprecate would be for simpler maintenance. I don't think iterator pairs will be seen as "wrong" before like 2030 though. I think the reason for the sub-namespace was to be able to redefine old functions, such as |
That's possible. I guess I'm not certain we should pull in |
No big difference as far as I can see. One thing to keep in mind is that |
https://en.cppreference.com/w/cpp/iterator/begin |
Yeah, it seems that |
I think we shouldn't use |
Ah yeah. Using |
I would prefer iterator pairs because they're more flexible and the C++ standard library is using it. I am also okay with an additional convenience function that takes in a container and uses ADL and forwards to the iterator pair version. However, maybe it would be better to not require the type of
instead of
so that sentinel end iterators can be accepted too. Also, if we're accepting any InputIterator (not just ForwardIterators), I believe we will need a specialization for the case when we get InputIterator. I think this is too much work to be justifiable, so I would recommend just mandating that users only give us ForwardIterators. Note that C++20 continues to introduce "fixes" to existing iterator pair algorithms (e.g. https://en.cppreference.com/w/cpp/algorithm/ranges/for_each), so I think iterator pairs will continue to be a "recommended" way of doing things even after Ranges is widely adopted. |
Yeah true, writing overloads for generic containers will be trivial if we have iterator overloads. Sounds good. |
Just to let you know, I'm considering implementing some iterator-ish API (e.g. something like |
Hmm okay, so would it make the functions opening multiple files/directories return an iterator instead? Would that create any issues with lifetime management? Or is the idea that you use the iterator like |
It would probably be something like this (https://github.com/btzy/nativefiledialog-extended/pull/35/files). Only the Windows backend is implemented currently, but I've checked the Mac and GTK backends and they have a similar enumerator interface. See the function signatures and comments in nfd.h. The general idea is that you can create an enumerator that references the underlying collection, and on that enumerator there is a GetNext() function that advances the enumerator and produces the next file path. (While implementing this, I'm starting to wonder if the Windows I'm open to suggestions about the API though. One possibility is to make it more flexible by decoupling advancing the iterator from dereferencing it (i.e. decoupling In C++ you should be able to implement |
If the PathSet refers to some underlying data, wouldn't it simply be able to write an iterator wrapper around a pointer, letting Iterator(nullptr); act as the sentinel? begin(PathSet) == {.ptr = /* some pointer */};
end(PathSet) == {.ptr = nullptr};
advance(Iterator) == Iterator::operator++();
get(Iterator) == Iterator::operator*();
//c++:
for_each(
begin(pathSet),
end(pathSet),
[](auto x) { std::cout << *x << "\n"; });
//c:
Iterator it = begin(pathSet);
while(it.ptr) {
printf("%s", get(it));
advance(it);
}
freePathSet(pathSet); Which seems to look pretty nice and idiomatic for both c and c++. |
If I understand you correctly, you're proposing two things:
That would work for GTK, but not so sure about Windows/MacOS: The underlying implementation of a PathSet may not always be a plain old linked list. In fact, it is only the case in GTK.
This means that the |
Hmm, I see. Looking a little at the docs for win. Just thinking out loud: class It {
IShellItem* current;
IEnumShellItems* enumerator;
public:
It(IShellItemArray* a) : enumerator(a->EnumItems()) {
next();
}
auto next() {
auto res = enumerator->next(1, current, nullptr);
if(res == S_FALSE) current = nullptr;
}
auto get() {
return getPath(current); // Don't know how you get the actual path from an IShellItem
}
} I'm not sure about the semantics, but hopefully it makes sense. Do you think something like that would be possible? |
It seems possible, though I think you'd need a few checks in case the list is empty. But I'm wondering if it's now easier to implement it directly in the C++ API instead of adding it to the C API. It seems that the main benefit of this is to decouple advancing from dereferencing. I'm thinking this seems to be a C++ specific thing, since C doesn't have the concept of iterators (and Rust iterators have a single Furthermore, it is possible to implement exactly what you propose directly inside the C++ header, using my proposed API in #35 (I didn't compile the following code so there might be bugs, but you get the idea): class It {
nfdpathsetenum_t enumerator;
nfdnchar_t* currentPath;
public:
It(nfdpathset_t* a) {
NFD_PathSet_GetEnum(a, &enumerator);
next();
}
It& operator++() {
NFD_PathSet_EnumNextN(&enumerator, ¤tPath);
return *this;
}
auto operator*() {
return currentPath;
}
}; It has a bit of extra overhead if the user doesn't dereference the iterator, but otherwise there is no other overhead. There would be negative overhead if the user dereferences the same value more than once. |
Yeah, that looks pretty good. Haven't done much C so don't know what's idiomatic there really. |
I don't think there's really a single idiomatic way to do it in C, because they have no concept of "iterators". Also, perhaps it might still be good to have an implementation using an empty sentinel for the end iterator, perhaps via an opt-in #define macro somewhere? Since it might yield speedups and is "blessed" by C++20. |
Hmm, because what I'm thinking is that if you want to keep the C++ API as thin as possible, you could implement the iterator in C and then just wrap that in a C++ class or something to make it compatible with algos and such. That would also make the library more uniform across the two languages, same semantics expressed differently. Yeah having a sentinel struct would simplify some of the code I think. I don't know how noticeable any speedup would be though, compared to the rest of the operation. |
I don't think we should be concerned about the library being more uniform across the two languages. My concern was not about wanting to keep the C++ API thin; it is more of not wanting to force users to use the C++ abstractions (e.g. std::string, std::filesystem) if they don't want it. Since I'm not intending to deprecate the existing C++ wrapper (for those who just want automatic destruction), it's okay for a slightly more complicated C++ wrapper as long as it doesn't introduce too much overhead. By the same argument, we should not force C users to "think in terms of C++ iterators" when the underlying platform implementation (i.e. the Windows/MacOS/GTK implementation of PathSet) behaves much more like a linked list / enumeration.
I agree, the speedup will probably be quite negligible. But it feels like an unnecessary transformation from iterator+sentinel (which is standard in C++20) to iterator+iterator, since the underlying platform implementation is already essentially iterator+sentinel. In any case, such a thing can be controlled with a compilation flag, and we can make it opt-in anyway, so we can ignore this and go with iterator+iterator for now. |
Ok, so do I understand correctly that one of the design goals for the C API is to be as close to the platforms API as possible? |
Yes, that is right :) All the overhead is only meant to abstract over platform-specific differences and provide a consistent API to the user. Actually, the current C++ API was also written with that goal in mind. |
If that's the case, maybe it's best to go with building other APIs around this, kind of like your original suggestion of using this as a submodule, |
Yeah, that works too. |
We could see it kind of like the role of the intermediate representation of compilation. We need to be able to support X platforms in Y languages. Doing it for every language directly would need X*Y implementations, but having this intermediate stage API in C/C++, it would only need X + Y implementations (one for every platform -> C, one for every C -> language). This library would then act as the translation from the different platforms to this intermediate API. |
The C++ wrapper atm is still (understandabley) pretty C-y.
Some things that could be improved would be to use
std::basic_string
instead ofunique_ptr
,and to use
std::filesystem::path
to represent paths.I understand that this could possibly mean API breaks, so I'm wondering how backwards compatible this project tries to be?
We could also use
[[deprecated(reason)]]
to faze out old functions if that would help.One issue could possibly be the need to maintain two separate APIs for C and C++, as they'd diverge more.
I'm thinking about forking this and just do away with all the C, but I'd rather contribute to an already existing repo if it's possible :^)
The text was updated successfully, but these errors were encountered: