Is it possible to change behavior of function based on scope?
Asked Answered
B

3

6

I would like to create something similar to rust unsafe scope in C++. The idea is that I have some functions performing number of checks. For example:

void check() {
     if (...)
        throw exception(...);

}

void foo() {
     check();

     // do some work
}

Now, I want to be able to call function foo() with or (in different context) without performing those checks. Ideally it would look like this:

foo(); // call foo and perform checks
unsafe {
    foo(); // call foo without checks
}

My question is, is it possible to achieve something like this in compile time? Is it possible to somehow check (or act differently) from check function in what scope it is called?

I came up only with a runtime solution: to wrap it in some lambda:

unsafe([&] {
    foo();
});

where unsafe is implemented as follows:

void unsafe(std::function<void()> f)
{
     thread_local_flag = unsafe;
     f();
     thread_local_flag = safe;
}

check() function would just check for the thread_local flag and perform checks only when it is set to safe.

Biskra answered 30/11, 2018 at 8:15 Comment(4)
Probably related Is it possible to detect whether a local variable declared?Nidianidicolous
It's similar but, in my case check() function is defined in some other place and would not have access to variables declared when using unsafeBiskra
Why not just use two versions of foo? The unsafe version could be an overload, a different template instance, or simply depend on a parameter.Clive
Well, in the example I presented I could do that, but in general, I can call some other function bar inside unsafe which in turn calls foo. And I don't want to specialize bar and possible other methods.Biskra
O
6

🤔

namespace detail_unsafe {
    thread_local int current_depth;

    struct unsafe_guard {
        unsafe_guard()  { ++current_depth; }
        ~unsafe_guard() { --current_depth; }

        unsafe_guard(unsafe_guard const &) = delete;
        unsafe_guard &operator = (unsafe_guard const &) = delete;
    };
}

#define unsafe \
    if(::detail_unsafe::unsafe_guard _ug; false) {} else

bool currently_unsafe() {
    return detail_unsafe::current_depth > 0;
}

See it live on Coliru. Also, please don't actually define unsafe as a macro...

Orthohydrogen answered 30/11, 2018 at 10:7 Comment(2)
'My question is, is it possible to achieve something like this in compile time?' Just be aware that this still is a runtime solution (@Igor).Churchman
@Churchman I really should take more time to read questions... Anyway, I don't think this is possible at compile time in the general case, because multiple TUs can be involved and communication between TUs at compile-time is severely restricted...Orthohydrogen
C
2

is it possible to achieve something like this in compile time?

Not the way you presented. Making foo a template function might give you equivalent results, though:

enum class CallType // find a better name yourself...
{
    SAFE,
    UNSAFE,
};

template <CallType Type = CallType::SAFE>
void foo()
{
    if constexpr(Type != CallType::UNSAFE)
    {
        if (...)
            throw ...;
    }
    // do some work
}

You might call it like:

foo();
foo<CallType::UNSAFE>();

Disliking templates?

Simple approach (thanks, @VTT):

void check(); // no template any more

void foo_unsafe()
{
    // do some work
}
inline void foo()
{
    check();
    foo_unsafe();
}

Or selecting via parameter (this pattern exists in standard library, too):

struct Unsafe
{
};
inline Unsafe unsafe;

void check();

void foo(Unsafe)
{
    // do some work
}
inline void foo()
{
    check();
    foo(unsafe);
}

Edit:

Well, in the example I presented I could do that, but in general, I can call some other function bar inside unsafe which in turn calls foo. And I don't want to specialize bar and possible other methods.

Unter this constraint, the template variant might be the closest you can get to at compile time; you don't have to specialise all the functions, but you'd need to make templates from:

template <CallType Type = CallType::SAFE>
void bar()
{
    // do some other work
    foo<Type>(); // just call with template parameter
    // yet some further work
}
Churchman answered 30/11, 2018 at 9:21 Comment(0)
F
1

I would simply use a RAII type to toggle the unsafe flag inside a scope as such:

thread_local bool unsafe_flag = false;

/// RAII Type that toggles the flag on while it's alive
/// Possibly add a reference counter so it can be used nested
struct unsafe_scope
{
    constexpr unsafe_scope() { unsafe_flag = true; }
    ~unsafe_scope()          { unsafe_flag = false; }
};

/// Gets a value from a pointer
int get_value(int* ptr)
{
    if ( unsafe_flag )
    {
        if ( ptr == nullptr ) { return 0; }
    }

    return *ptr;
}

int main()
{
    int* x = nullptr;

    //return get_value(x); // Doesn't perform the check

    {
        unsafe_scope cur_scope;
        return get_value(x); // Performs the check
    }
}

In order to make it nested I would add a reference counter like this:

/// RAII Type that toggles the flag on while it's alive
struct unsafe_scope
{
    thread_local static size_t ref_count;

    constexpr unsafe_scope()
    {
        unsafe_flag = true;
        ref_count++;
    }
    ~unsafe_scope()
    {
        ref_count--;
        if ( ref_count == 0 ) { unsafe_flag = false; }
    }
};

/// In source file
thread_local size_t unsafe_scope::ref_count = 0;

The ref_count doesn't need to be atomic since it's thread_local

Now I don't think there's a way to achieve the syntax you wanted with the unsafe before the scope, but if you put it right after the scope as such it should be about the same:

{ unsafe_scope cur_scope;
    return get_value(x); // Performs the check
}

Edit:

I've now noticed Quentin's answer is also a RAII type, just with slightly different semantics, instead of having a global thread_local flag a function just returns if the reference counter is bigger than 0. Also the macro achieves the exact syntax you wanted, although it's also possible with this unsafe_scope by modifying his macro like this:

#define unsafe\
    if (unsafe_scope cur_scope; false) {} else 

His method uses C++17's if initializer, which lets you initiates a variable in the if statement, but the variable is still initialized in the else block, so it only gets destroyed after the else scope if over.

Flagella answered 30/11, 2018 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.