To expand on skypjack's comment: Yes, if you're okay with annotating every disallowed function, so that non-annotated functions are allowed by default. This is called the Passkey Idiom. Godbolt:
// Any object of type TakesLocks serves as a passkey
// for functions that take mutex locks
//
class TakesLocks {
friend int main();
explicit TakesLocks() {} // private constructor
};
void allowedFunction();
void disallowedFunction(TakesLocks);
//Only allowed to call allowed functions.
void foo() {
allowedFunction();
disallowedFunction(TakesLocks()); // error, can't call private ctor
}
//Is allowed to call any function it wants.
void bar(TakesLocks tl) {
allowedFunction();
disallowedFunction(tl); // No error
}
int main() {
foo();
bar(TakesLocks()); // OK, main is a friend of TakesLocks
}
This is useful if you have a "closed" blacklist of disallowedFunction
s, such that you can enumerate them all, and you control their signatures. But in real life, we'd also want e.g. TakesLocks
to be required in order to call printf
, and strftime
, and so on — there's no way for us to enforce that. So TakesLocks
is basically the worst example I could have chosen. :)
For another approach, much closer to what you originally asked, that has had great success in academia but never shipped in any mainstream compiler as far as I know, see Jeffrey Scott Foster's Cqual.
EDIT: Just recently I learned that Clang supports the reverse approach via the enforce_tcb
attribute! "TCB" stands for "Trusted Compute Base," implying that anything inside the TCB must never call anything outside the TCB. Compile with -Werror=tcb-enforcement
: Godbolt. Use this approach if you're okay with limiting enforcement to Clang and if you're okay with annotating every allowed function, so that non-annotated functions are disallowed by default. (This is likely too restrictive, because it means e.g. you can't call puts
or sin(long double)
or std::copy
from an annotated function. Vice versa, Clang quietly whitelists a handful of library functions, like memcpy
, sin(double)
, and std::move
, into every TCB; which might also be problematic, depending on your use-case.)
// A function inside the TakesNoLocks TCB must not call
// any function outside that TCB.
//
[[clang::enforce_tcb("TakesNoLocks")]] void allowedFunction();
void disallowedFunction();
//Only allowed to call allowed functions.
[[clang::enforce_tcb("TakesNoLocks")]] void foo() {
allowedFunction();
disallowedFunction(); // error, can't call outside the TCB
}
//Is allowed to call any function it wants.
void bar() {
allowedFunction();
disallowedFunction(); // No error
}
int main() {
foo();
bar(); // OK, main is outside the TCB itself
}