Getting the const-correctness of Object sorted once and for all

Chad J chadjoan at __spam.is.bad__gmail.com
Tue May 15 11:06:45 PDT 2012


On 05/13/2012 12:39 PM, Stewart Gordon wrote:
> http://d.puremagic.com/issues/show_bug.cgi?id=1824
>
> This has gone on for too long.
>
> Object.toString, .toHash, .opCmp and .opEquals should all be const.
> (It's also been stated somewhere that they should be pure and nothrow,
> or something like that, but I forget where.)
>
> This makes it a nightmare to use const objects in data structures, among
> other things, at best forcing the use of ugly workarounds. There are
> probably other, more serious effects of this that can't easily be worked
> around.
>
> It seems that the main obstacle is rewriting the relevant methods in
> std.stream. The current implementation doesn't make sense anyway -
> reading the entire contents of a file is certainly not the way to
> generate a hash or string representation of the stream. I'm thinking the
> hash should probably be the stream handle, and the string representation
> could perhaps be the full pathname of the file. Of course, what it
> should be for non-file streams is another matter. (This would be a
> change at the API level, but when the API's as fundamentally flawed as
> this....)
>
> Are there any other bits of druntime/Phobos that need to be sorted out
> before these methods can be declared const/pure/nothrow once and for all?
>
> Stewart.

I haven't read all of the replies here, but the gist I'm getting is that 
we have two contradictory interests:
(1.)  .toString(), .toHash(), .opCmp(), .opEquals(), should be 
const/pure/nothrow because their operations are inherently 
const/pure/nothrow and it would be both unintuitive and less reusable if 
they weren't.
(2.)  Marking these as const/pure/nothrow prevents caching and 
optimizations that are important for real-world code.

When I see a dichotomy like this forming, I have to think that we're 
missing something.  There is definitely a better way!  I, for one, 
wouldn't give up until it's found.



So I'll toss out an idea:

I think the const we want is a kind of "interface const" rather than an 
"implementation const".  Interface const means that calling the method 
will not cause any /observable/ state-changes in the referred object. 
Implementation const is stricter: it means that calling the method will 
not cause ANY state-changes in the referred object at all.

I am going to be fairly strict about my notion of observable.  Changes 
to private members are observable:

class Foo
{
	private string strCache = null;

	// The isCaching method makes strCache "observable".
	public bool isCaching()
	{
		if ( strCache is null )
			return false;
		else
			return true;
	}

	public string toString()
	{
		if ( strCache is null )
		{
			// Observable change in strCache!
			// ... because isCaching reveals it
			//   to everyone.
			strCache = someComplicatedCalculation();
			return strCache;
		}
		else
			return strCache;
	}
}


An idea I thought of is to introduce a method local declaration that 
allows a method to access instance-specific-state that isn't accessible 
to the rest of the class:

class Foo
{
	// The isCaching method is no longer possible.

	public pure nothrow string toString() const
	{
		// strCache gets stored in an instance of Foo
		// strCache is only accessable in this method body.
		@instance string strCache = null;

		if ( strCache is null )
		{
			// Observable change in strCache!
			// ... because isCaching reveals it
			//   to everyone.
			strCache = someComplicatedCalculation();
			return strCache;
		}
		else
			return strCache;
	}
}

Now it is not possible (or at least, not very easy at all) for the 
statefulness of strCache to leak into the rest of the class (or 
program).  It is not "observable".  If the implementor does their 
caching wrong, then the statefulness might be observable from the 
toString method, but nowhere else (except for methods that call 
toString).  It's not a perfect situation, but it's /a lot/ better.  We 
may be required to trust the implementor a little bit and assume that 
they know how to make sure strCache's statefulness isn't observable (ex: 
two calls to toString() should return the same results).

To communicate intents to invalidate the cache:

class Foo
{
	private toStringCacheValid = false;

	public void methodThatInvalidatesCache()
	{
		...
		toStringCacheValid = false;
	}

	public pure nothrow string toString() const
	{
		// strCache gets stored in an instance of Foo
		// strCache is only accessable in this method body.
		@instance string strCache = null;

		if ( !toStringCacheValid )
		{
			// Observable change in strCache!
			// ... because isCaching reveals it
			//   to everyone.
			strCache = someComplicatedCalculation();
			toStringCacheValid = true;
			return strCache;
		}
		else
			return strCache;
	}
}

This demonstrates how the cache does not need to be touched directly to 
be invalidated.  The information flows from the body of the class and 
into the cache, and not the other way around.

This may even have implications that interface-constness could be 
(closely, but not perfectly) maintained by offering write-only 
declarations in the class body that can only be read by the const method 
that uses them.

Perhaps something along these lines would satisfy both needs?


More information about the Digitalmars-d mailing list