const-correctness and the safe bool idiom
Asked Answered
J

3

11

I have another question related to the safe bool idiom:

typedef void (Testable::*bool_type)() const;             // const necessary?
void this_type_does_not_support_comparisons() const {}   // const necessary?

operator bool_type() const
{
    return ok_ ? &Testable::this_type_does_not_support_comparisons : 0;
}

How come the bool_type (the typedef) and this_type_does_not_support_comparisons are const? Nobody is supposed to actually call the member function through the return pointer anyway, right? Is const necessary here? Would operator bool_type (the member function) violate const-correctness otherwise?

Jampan answered 11/8, 2011 at 15:0 Comment(3)
My bet is that const is not necessary. A body for the function is not either, since you're not supposed to call the function anyway.Dwyer
@Alex: But a member function without a body has no address, right? It doesn't compile without a body on VC10, at least.Jampan
hmm you're right. I just checked my safe_bool template I have deep in my VS2010 current project and it has a body. It has const too.Dwyer
L
5

The "safe bool idiom" is the technical answer to the question "i want a vehicle that is both sports car and tractor, and maybe a boat". The practical answer is not the technical answer…

That said, the problem it solves is just to give a result that is convertible to bool but not to much of anything else (otherwise an instance of the class could be passed as actual argument where e.g. the formal argument was int, say). A data pointer could be convertible to void*. A function pointer isn't, at least formally within the C++ standard (Posix is something else, practice also).

Using a member function pointer protects against accidentally calling the function, given the pointer from the safe bool operator. The const constricts it a bit, but if fate has put someone on the path of making maximum number of silly mistakes, that person might still manage to call the do-nothing function. Instead of the const I think I would just let it have an argument of a private type, where other code cannot provide such an argument, and then it doesn't have to be a silly member function type anymore.

Can look like this:

#include <stdio.h>

class Foo
{
private:
    enum PrivateArg {};
    typedef void (*SafeBool)( PrivateArg );
    static void safeTrue( PrivateArg ) {}

    bool    state_;

public:
    Foo( bool state ): state_( state ) {}

    operator SafeBool () const
    { return (state_? &safeTrue : 0); }
};

int main()
{
    if( Foo( true ) ) { printf( "true\n" ); }
    if( Foo( false ) ) { printf( "false\n" ); } // No output.

    //int const x1 = Foo( false );        // No compilado!
    //void* const x2 = Foo( false );      // No compilado!
}

Of course, the practical answer is instead something like this:

#include <stdio.h>

class Foo
{
private:
    bool    isEmpty_;

public:
    Foo( bool asInitiallyEmpty )
        : isEmpty_( asInitiallyEmpty )
    {}

    bool isEmpty() const { return isEmpty_; }
};

int main()
{
    if( Foo( true ).isEmpty() ) { printf( "true\n" ); }
    if( Foo( false ).isEmpty() ) { printf( "false\n" ); } // No output.

    //bool const x0 = Foo( false );       // No compilado!
    //int const x1 = Foo( false );        // No compilado!
    //void* const x2 = Foo( false );      // No compilado!
}

Summary wrt. questions asked:

  • How come the bool_type (the typedef) and this_type_does_not_support_comparisons are const?

Somebody didn't quite understand what they coded. Or maybe they intended to restrict the ability to call, a little. But then, pretty futile measure.

  • Nobody is supposed to actually call the member function through the return pointer anyway, right?

Right.

  • Is const necessary here?

No.

  • Would operator bool_type (the member function) violate const-correctness otherwise?

No.

Cheers & hth.,

Lifer answered 11/8, 2011 at 15:36 Comment(7)
"I think I would just let it have an argument of a private type", oh wait, I retract my previous statement, this is brilliant!Jampan
I wrote something wrong about the const, somehow confusing black and white. I just strike it out. Sorry -- the rest is OK though.Lifer
You can still (stupidly, but this is the point of the exercise) declare a free function with signature void (Foo::PrivateArg) and compare it to the result of operator SafeBool. I think the SafeBool type must be a pointer to member function after all.Dwyer
@Alexandre C: Regarding your comment "You can still (stupidly, but this is the point of the exercise) declare a free function with signature void (Foo::PrivateArg) and assign its address or compare it to the result of operator SafeBool". Anyone who does use a private type in that way, deserves whatever is coming. As I see it it is out of the realm of practical programming.Lifer
@Alexandre: I don't know, but an easy way to check is to put the code to Comeau Online compiler.Lifer
@Alf: Another point for pointer to member functions: pointer to functions can be compared with < and relatives. Pointer to member functions cannot. There is the possibility that someone writes x < y inadvertantly. You could declare those operators somewhere and have them trigger an error, but I find it more practical with member functions not to have to do this.Dwyer
"i want a vehicle that is both sports car and tractor, and maybe a boat" - you want a TransformerHitoshi
D
1

8.3.5/ A cv-qualifier-seq shall only be part of the function type for a nonstatic member function, the function type to which a pointer to member refers, or the top-level function type of a function typedef declaration. The effect of a cv-qualifier-seq in a function declarator is not the same as adding cv-qualification on top of the function type, i.e., it does not create a cv-qualified function type.

If I read correctly, you can return a pointer to non const member in a const member function. You just won't be able to call it with a non const object.

A way to forbid the calling is:

private:
    struct private_ 
    {
        void this_type_does_not_support_comparisons() {}
    };

public:
    typedef void (private_::*bool_type)() const;

    operator bool_type() const
    {
        return ok_ ? &private_::this_type_does_not_support_comparisons : 0;
    }

Pointer to member functions can still be compared for equality. You have to write an operator== and operator!= for Testable::bool_type types which trigger an error. Easier to do with the CRTP form of the safe bool idiom since those operators become templates and thus can have an erroneous body.

Example:

template <typename T>
class safe_bool_concept
{
    // Implementation detail of safe bool
protected:
    ~safe_bool_concept() {}

public:
    operator safe_bool() const
    {
        return static_cast<const T*>(this)->is_null() ? ...;
    }
};

struct Foo : safe_bool_concept<Foo>
{
    ...

private:
    friend class safe_bool_concept<Foo>;
    bool is_null() const { ... }
};

then you can do (do the same with !=):

template <typename T>
void operator==(const safe_bool_concept<T>& x, const safe_bool_concept<T>&)
{
    x.some_private_member(); // invalid, but won't be generated
                             // unless safe_bool classes are compared
}

This means that the safe bool idiom should be implemented via CRTP if you want to forbid comparisons. Comparisons to zero will still work however.

If you go the non-member function route, you'll have to provide <, >, <= and >= too.

Dwyer answered 11/8, 2011 at 15:33 Comment(0)
R
1

Although this is old, the safe bool idiom has its historical significance and I decide to write a full answer because I really disagree with the accepted answer.

One important thing is that we cannot answer this question in isolation. The function is called this_type_does_not_support_comparisons for a reason. It is not just some random private member. The intention is clear: the function is also used to avoid accidental comparison between two testables. If for example we look at the implementation here, we can see that the function is actually called.

class Testable {
    bool ok_;
    typedef void (Testable::*bool_type)() const;
    void this_type_does_not_support_comparisons() const {}
  public:
    explicit Testable(bool b=true):ok_(b) {}

    operator bool_type() const {
      return ok_ ? 
        &Testable::this_type_does_not_support_comparisons : 0;
    }
};
template <typename T>
bool operator!=(const Testable& lhs, const T&) {
    lhs.this_type_does_not_support_comparisons();   
    return false;
}
template <typename T>
bool operator==(const Testable& lhs, const T&) {
    lhs.this_type_does_not_support_comparisons();
    return false;
}

Note first that this code will not compile on modern compilers because the body of the function is unconditionally ill-formed no matter what T is. However, let's consider it from a historical perspective. In older c++ versions this ensures that when operator== is called on two testables, and the comparison message nicely indicates the real issue. For example, GCC 4.1.2 generates the following error message.

error: 'void Testable::this_type_does_not_support_comparisons() const' is private

This is why the function is called this_type_does_not_support_comparison in the first place. The const need to be there because the function is actually called in a context where const needed, namely template <typename T> bool operator==(const Testable& lhs, const T&). Note that const cannot be removed from the first parameter because otherwise it cannot detect the comparison between a const object and another object and silent conversion to bool becomes possible again.

So here are my answers to the questions,

  • How come the bool_type (the typedef) and this_type_does_not_support_comparisons are const?

Because the function is actually called and the consts are necessary to suppress the implicit conversion to bool in the context of operator==. It's definitely not because someone did not understand what they wrote or to prevent the testables to be called with the (testable.*testable)() syntax (That is already a very exotic use and no one uses a bool like that, so that should not be considered an accidental misuse to be prevented at all. Moreover, if you try it, it doesn't actually compile).

  • Nobody is supposed to actually call the member function through the return pointer anyway, right?

Mostly right. The member functions are not supposed to be called from outside but are actually called in the comparison operators from the class itself. It is easy to miss this use because it is not included in the original snippet of the question.

  • Is const necessary here?

Again, no if we limit ourselves to the snippet posted, yes if we consider the broader context of how the canonical safe bool idiom is implemented.

  • Would operator bool_type (the member function) violate const-correctness otherwise?

No if we just consider the snippet posted. Yes in the broader context. In the answer by Alexandre C. it is already addressed why returning a pointer-to-non-const-member in a const-qualified member function does not violate the const-correctness.

Of course, it is 2023 now and if anyone has a modern compiler that supports c++11 they should just use explicit operator bool() and forget all these. However, there may still be poor guys who need to work with legacy environment restricted to c++03, and in that case maybe the answer by Alexandre C. provides a possibly better alternative. After all, the original version of safe bool idiom does not compile with new compilers even if the standard is set to c++03.

Rayleigh answered 26/11, 2023 at 8:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.