Can an object know its own constness?

2019-02-16 14:31发布

问题:

With decltype and std::is_const the constness of a variable can be externally detected. But is it also possible for an object to know its own constness? Usage should be like:

#include <type_traits>
#include <iostream>
#include <ios>

struct Test
{
    Test() {}

    bool print() const
    {
       // does not work as is explained in https://stackoverflow.com/q/9890218/819272
       return std::is_const<decltype(*this)>::value; // <--- what will work??
    }
};

int main()
{
    Test t;
    const Test s;

    // external constness test
    std::cout << std::boolalpha << std::is_const<decltype(t)>::value << "\n";
    std::cout << std::boolalpha << std::is_const<decltype(s)>::value << "\n";

    // internal constness test
    std::cout << std::boolalpha << t.print() << "\n";
    std::cout << std::boolalpha << s.print() << "\n"; // <--- false??
}

Output on LiveWorkSpace Is this somehow possible?

MOTIVATION: I want to be able to detect whether a const member function is invoked on a const object or is coming from a non-const object. The object could e.g. represent a cache and the member a view. If the cache was const, one could presumably use an optimized draw routine, whereas if the underlying data was non-const, the draw routine would need to do periodically check if the data was refreshed.

NOTE: the related question asks how to break the build for const objects, but I don't quite understand if the answer to that implies a definite NO for my question. If not, I want to capture the constness in a boolean for further usage.

EDIT: as was pointed out @DanielFrey, the constructor is not a good place to test for constness. What about a const member function?


UPDATE: Thanks everyone for correcting my initially ill-posed question and providing the various parts of the answer (constructors' ill-defined constness, rvaluedness of this, the contextual meaning of const, the -with hindsight- obvious overload trick that I had overlooked, and the const reference aliasing loophole lurking in the shadows). For me this question was Stackoverflow at its best. I decided to select @JonathanWakely's answer because it showed how to define Mutable and Immutable classes that strengthen the constness concept to achieve what I want in a foolproof way.

回答1:

As others have stated, you cannot tell if an object was declared as const from within a member function. You can only tell if it's being called in a const context, which is not the same.

MOTIVATION: I want to be able to detect whether a const member function is invoked on a const object or is coming from a non-const object. The object could e.g. represent a cache and the member a view. If the cache was const, one could presumably use an optimized draw routine, whereas if the underlying data was non-const, the draw routine would need to do periodically check if the data was refreshed.

You can't tell that reliably.

struct A
{
  void draw() { fut = std::async(&A::do_draw, this, false); }
  void draw() const { fut = std::async(&A::do_draw, this, true); }
  void update(Data&);
private:
  void do_draw(bool this_is_const) const;
  mutable std::future<void> fut;
};

A a;
const A& ca = a;
ca.draw();           // object will think it's const, but isn't
Data new_data = ...;
a.update(new_data);  // do_draw() should recheck data, but won't

You can model it in the type system, by defining separate mutable and immutable types.

struct Base
{
  virtual ~Base();
  virtual void draw() const = 0;
protected:
  void do_draw(bool) const;
};

struct MutableCache : Base
{
  virtual void draw() const { fut = std::async(&Base::do_draw, this, false); }
  void update();
};

struct ImmutableCache : Base
{
  virtual void draw() const { fut = std::async(&Base::do_draw, this, true); }
  // no update function defined, data cannot change!
};

Now if a cache is create as an ImmutableCache you know it can't change, so that replaces your previous idea of a "const" object. A MutableCache can change, so needs to check for refreshed data.



回答2:

It's not possible for constructors (the original question) because of

12.1 Constructors [class.ctor]

4 A constructor shall not be virtual (10.3) or static (9.4). A constructor can be invoked for a const, volatile or const volatile object. A constructor shall not be declared const, volatile, or const volatile (9.3.2). const and volatile semantics (7.1.6.1) are not applied on an object under construction. They come into effect when the constructor for the most derived object (1.8) ends. A constructor shall not be declared with a ref-qualifier.

For member functions (the current question), you could simply provide both a const and a non-const overload, forward both to a (private) method that takes the constness as a boolean template parameter.



回答3:

The type of this (and consequently of *this) is purely determined by the cv-qualifier of the function and does not change depending on whether the actual object is cv-qualified or not.

§9.3.2 [class.this] p1

In the body of a non-static (9.3) member function, the keyword this is a prvalue expression whose value is the address of the object for which the function is called. The type of this in a member function of a class X is X*. If the member function is declared const, the type of this is const X*, if the member function is declared volatile, the type of this is volatile X*, and if the member function is declared const volatile, the type of this is const volatile X*.

So, you can't see inside of a member function whether the object it's invoked on is const, but you can make the compiler call different functions depending on constness:

struct X{
  void f(){ /* non-const object */ }
  void f() const{ /* const object */ }
};

int main(){
  X x1;
  X const x2;
  x1.f(); // calls non-const 'f'
  x2.f(); // calls const 'f'

  // but beware:
  X const& rx = x1;
  rx.f(); // calls const 'f', because the "object" it's invoked on is const
}

Beware of the limitation laid out in the snippet, though.



回答4:

what will work??

Nothing will work.

The object running its constructor is never const. It may only be assigned to a const variable after the constructor has run its course.

It also can't be determined for member functions (non-const member functions included, since const_cast may have been used)

Const-ness is a property that exists at each call site, not as a property of the function body itself.

MOTIVATION: I want to be able to detect whether a const member function is invoked on a const object

You can't do that, but you can come close...

class C
{
public:
    void func() const
    {
        std::cout << "const!";
        func_impl();
    }
    void func()
    {
        std::cout << "non-const!";
        func_impl();
    }
private:
    void func_impl() const;
};

The object could e.g. represent a cache and the member a view. If the cache was const, one could presumably use an optimized draw routine, whereas if the underlying data was non-const, the draw routine would need to do periodically check if the data was refreshed.

That would be an unreliable usage of const because const is not a property of the object itself. It's a property of the current context that the object's being used.

Detecting that it's const in the current context doesn't tell you that the object has always been treated in a const context.



回答5:

I don't know if it's possible in that way, by means of a member value that's set in the constructor, but the object can report its const-ness with member functions:

struct Test
{
  bool is_const() const
  {
    return(true);
  }

  bool is_const()
  {
    return(false);
  }
};


回答6:

This isn't possible because it's possible for a particular value to be simultaneously viewed as const and not const. Consider

MyType t = ...;
MyType& nonConstRef = t;
const MyType& constRef = t;

At this point t has both a const and a non-const reference.



回答7:

Detecting constness within an object isn't possible, but you state that your motivation is…

“ I want to be able to detect whether a const member function is invoked on a const object or is coming from a non-const object.”

Well that’s easy, just provide a non-const overload.

The overloads can defer to a common implementation, e.g. as follows:

#include <type_traits>
#include <iostream>
using namespace std;

class Test
{
private:
    template< class CvTest >
    static void print( CvTest& o )
    {
        cout << boolalpha;
        cout << "> object says: Hey, const = " << std::is_const<CvTest>::value << "!" << endl;
    }

public:
    bool print()        { return (print( *this ), false); }
    bool print() const  { return (print( *this ), true); }
};

int main()
{
    Test t;
    const Test s;

    cout << "External constness test:" << endl;
    cout << boolalpha << is_const<decltype(t)>::value << "\n";
    cout << boolalpha << is_const<decltype(s)>::value << "\n";

    cout << endl;

    cout << "Internal constness test:" << endl;
    cout << boolalpha << t.print() << "\n";
    cout << boolalpha << s.print() << "\n";
}

Results:

External constness test:
false
true

Internal constness test:
> object says: Hey, const = false!
false
> object says: Hey, const = true!
true