@Jan sent me a private note with some suggestions and questions. I’ll paraphrase his comments here. (And a few I’ll put as “code review” comments)
All classes in the end embed (only) a C handle. Doesn’t it make sense
to have the same method for accessing this, e.g. c, so we can do
PlFunctor f;
...
PL_something(f.c(), ...)
I know it is common to provide access functions for members to keep
binary compatibility, but as this is only inline code I doubt that makes
much sense, so f.c and c as a public member would do as well?
The general convention with C++ is for struct to be for POD (plain old data) that might have some convenience functions and class is for everything else; the biggest difference is whether the default visibility is “public” (struct) or “private” (class); and typically fields in a class follow a convention (suffix “_” at Google, prefix “m_” at GNU_, etc.). The C++ wrappers in SWI-cpp.h seem to be half-way in between – they’re POD in the sense that they just wrap a handle, but they provide quite a few “convenience” functions, so I’ve decided that they should be “class”.
As Jan says, there’s no need for getters/setters for binary compatibility; the question is how to access the underlying handle when needed. The compiler will expand PL_something(f.c(), ...) to PL_something(f.functor_, ...) so efficiency isn’t the issue. In general, when someone wants to access a PL_something() in SWI-Prolog.h, we should want to make that available in SWI-cpp.h (as f.something(...)), so the convenience of accessing the wrapped handle shouldn’t be a significant issue.
Perhaps leave the handle private and access it by C()? Making the method name upper-case would also make it easy to find with a regular text editor.
Why PlAtom::reset() and PlTerm::reset()?
I’m following the style of “smart pointers”, by having a function for setting the value to “null” (for atom_t and term_t, the “null” is 0, although I don’t think it’s documented anywhere). I forgot to include PlAtom::reset(atom_t) and PlTerm::reset(term_t). [I’ve also created “null” constants – they’re “private” for now.].
If we access the wrapped handle using C(), then we need a method for setting the value. If we make the wrapped handle public (as in the original SWI-cpp.h, then there’s no need for C() and reset(). I don’t have a strong opinion on this; the atom and term wrappers aren’t really smart pointers, so it could be confusing to follow the smart pointer conventions.
(We could, of course define a dereference operator (*) instead of C(), but I think that would be confusing because atom_t and term_t don’t always act like pointers in the SWI-Prolog.h functions.
some questions about is_valid()
The idea is that a constructor should be written like this:
PlAtom::PlAtom(const char *text)
: handle_(PL_new_atom(text)) { ... }
and not like this:
PlAtom::PlAtom(...) {
handle_ = PL_new_atom(text);
....
}
If the system guarantees that PL_new_atom() always succeeds or else terminates the program with a fatal error, then there’s no need for is_valid() in the constructor.
If PL_new_atom() fails due to out-of-memory, does that always give a fatal error? From looking at various bits of foreign function code and swipl internal code, it appears that a “resource error” is a possibility; and that’s why I added the test to the constructors.
However, there’s a second use for is_valid() – some code uses a “null” value (that is: 0) to indicate that the value hasn’t been set. We could create a operator bool() to check for validity, but there is also a “get_bool” defined for terms (which calls PL_get_bool(), which checks whether the term is an atom with the value true or false (and throws an exception if it’s any other value). This is confusing, so I decided to remove the operator bool() and require the check to be explicit.
[BTW, Jan originally wrote PlAtom constructor an initializer list, but some other constructors weren’t that way and I’ve modified them to use an initializer list where possible]
I’m unsure about the unify_*_ex() functions. Seems a lot of stuff that is barely ever needed. Why not PlTerm::must_be_unbound()? You typically only need that for returning blobs, so it could also be part of a C++ blob infrastructure.
When I was applying my changes to users of SWI-cpp.h, I found a number of places where unification was assumed to succeed. However, Jan told me in a private email that a check is always needed because even when unifying with a newly created term (from PL_new_term_ref(), which is encapsulated in the PlTerm_var constructor), there can be failure, typically from out-of-memory.
So, I annotated the regular unification methods with [[nodiscard]] to catch such errors and added the “must succeed” methods that throw an exception if they fail (with the suffix “_ex”). If my understanding is wrong, I could remove the “_ex” methods, and programmers can use write, e.g., (void)t.unify_term(t2) to turn off the warning.