web-dev-qa-db-fra.com

Est-il possible de changer le comportement d'une fonction en fonction de la portée?

Je voudrais créer quelque chose de similaire à Rust unsafe scope en C++ . L’idée est que certaines fonctions effectuent un certain nombre de contrôles. Par exemple:

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

}

void foo() {
     check();

     // do some work
}

Maintenant, je veux pouvoir appeler la fonction foo () avec ou (dans un contexte différent) sans effectuer ces vérifications. Idéalement, cela ressemblerait à ceci:

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

Ma question est la suivante: est-il possible de réaliser quelque chose comme cela en temps de compilation? Est-il possible en quelque sorte de vérifier (ou d'agir différemment) de la fonction check dans quelle étendue il est appelé?

Je n’ai proposé qu’une solution d’exécution: pour l’envelopper dans du lambda:

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

où unsafe est implémenté comme suit:

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

la fonction check () vérifie simplement l'indicateur thread_local et n'effectue des vérifications que si elle est définie sur safe.

6
Igor

????

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;
}

Voir en direct sur Coliru . De plus, veuillez ne pas définir unsafe comme une macro ...

6
Quentin

est-il possible de réaliser quelque chose comme ça en temps de compilation?

Pas comme tu l'as présenté. Faire de foo une fonction de modèle pourrait vous donner des résultats équivalents, cependant:

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
}

Vous pourriez l'appeler comme:

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

Vous n'aimez pas les modèles?

Approche simple (merci, @ VTT ):

void check(); // no template any more

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

Ou en sélectionnant via un paramètre (ce modèle existe aussi dans la bibliothèque standard):

struct Unsafe
{
};
inline Unsafe unsafe;

void check();

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

Modifier:

Eh bien, dans l'exemple que j'ai présenté, je pourrais le faire, mais en général, je peux appeler une autre barre de fonctions à l'intérieur de unsafe, qui à son tour appelle foo. Et je ne veux pas spécialiser bar et autres méthodes possibles.

Sous cette contrainte, la variante de modèle peut être la plus proche que vous puissiez obtenir au moment de la compilation compile; vous n'avez pas besoin de specialiser toutes les fonctions, mais vous devez créer des modèles à partir de:

template <CallType Type = CallType::SAFE>
void bar()
{
    // do some other work
    foo<Type>(); // just call with template parameter
    // yet some further work
}
2
Aconcagua

J'utiliserais simplement un type RAII pour basculer l'indicateur non sécurisé dans un scope en tant que tel:

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
    }
}

Afin de le rendre imbriqué, je voudrais ajouter un compteur de référence comme ceci:

/// 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;

Le ref_count n'a pas besoin d'être atomique puisqu'il est thread_local

Maintenant, je ne pense pas qu'il y ait un moyen d'obtenir la syntaxe que vous vouliez avec unsafe avant le scope, mais si vous le placez juste après le scope en tant que tel, cela devrait être à peu près le même:

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

Modifier:

J'ai maintenant remarqué que la réponse de Quentin est également un type RAII, avec une sémantique légèrement différente. Au lieu d'avoir un indicateur global thread_local, une fonction est simplement renvoyée si le compteur de référence est supérieur à 0. La macro exécute également la syntaxe exacte souhaitée, bien que c'est aussi possible avec ce unsafe_scope en modifiant sa macro comme ceci:

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

Sa méthode utilise l'initialisation if de C++ 17, ce qui vous permet d'initier une variable dans l'instruction if, mais la variable est toujours initialisée dans le bloc else, de sorte qu'elle n'est détruite qu'après la portée else, si elle est terminée.

1
Filipe Rodrigues

Il est également possible d'utiliser template partialization partielle à la place du préprocesseur.

Par exemple:

#include <iostream>
#include <type_traits>
#include <system_error>

template<class type>
struct unsafe
{};

template<>
struct unsafe<std::true_type>
{
private:
    static void check_min() {}

    template<typename T,typename ... Rest>
    static void check_min(T first,Rest...rest) {
        if(first < 0)
            throw std::system_error( std::make_error_code(std::errc::invalid_argument) );
        unsafe::check_min( rest... );
    }
public:
 template<class C,typename ... Args>
 void operator()(C callback,Args... args) {
    check_min( args... );
    callback( args... );
 }
};

template<>
struct unsafe<std::false_type>
{
 template<class C,typename ... Args>
 void operator()(C callback,Args...args) {
    callback( args... );
 }
};

class safe_context {
    safe_context(const safe_context&) = delete;
    safe_context& operator=(const safe_context&) = delete;
public:
    static thread_local bool _context;
public:

    constexpr safe_context() noexcept
    {}

    template<class C,typename ... Args>
    void operator()(C callback,Args...args) {
        if( _context )
            unsafe< std::false_type >()(callback, args... );
        else {
            unsafe< std::true_type >()(callback, args... );
            _context = true;
        }
    }
};

thread_local bool safe_context::_context = false;

int main ()
{

    safe_context ctx;

    // check with wrong args
    try {
    ctx( [](float x, float y, float z) {
            std::cout << '{' << x << ',' << y << ',' << z << '}' << std::endl;
        }  , 1.0F, -1.0F, 1.0F);
    } catch( std::exception& exc) {
        std::clog << exc.what() << std::endl;
    }

    ctx( [](int x, int y, int z) {
            std::cout << '{' << x << ',' << y << ',' << z << '}'<< std::endl;
         },
    1, 0, 1);

    // will not trow, even when args are wrong
    ctx( [](float x, float y, float z) {
            std::cout << '{' << x << ',' << y << ',' << z << '}' << std::endl;
        }  , 1.0F, -1.0F, 1.0F);

    return 0;
}

Les sorties:

Invalid argument
{1,0,1}
{1,-1,1}

Process returned 0 (0x0)   execution time : 0.053 s
Press any key to continue.
0
Victor Gubin