MATLAB: Overriding subsref and subsasgn – effect on private properties

classoopsubsasgnsubsref

I'm overriding subsref and subsasgn for a class. I want to influence the behaviour of obj(…), but I can't find a good way to do this without also either breaking obj.name property access, or breaking the privacy attributes for the properties.
Examples in the documentation (see "A Class with Modified Indexing") suggest that I check the type of indexing within my subsref/subsasgn functions and call the builtin subsref/subsasgn if the type is '.'. The problem is that because these are called from a class method, the access protection on properties is overriden – so I can access and update private properties from outside the class, as if the protection was not there.
Here's an example class
classdef test
properties (Access = private)
x = 'ought to be private'
end
methods
function v = subsref(M, S)
switch S.type
case '()'
v = 'this is ok';
case '{}'
error('{} indexing not supported');
case '.'
v = builtin('subsref', M, S); % as per documentation
end
end
end
end
and here's what goes wrong when I use it:
>> t = test
t =
test with no properties.
Methods
>> t(1)
ans =
this is ok
>> t.x
ans =
ought to be private
The attempt to access t.x should not succeed.
One solution I can think of is to write set.x and get.x methods for every single private property, to reimplement the protection that the Access attribute ought to provide.
[EDIT – added 16 hours after original post] Another possible solution: write code to analyse the subscript argument, consulting meta.property, before calling the builtin subsref/subsasgn. Not that hard, but it's ugly and probably inefficient to reimplement existing functionality.
Does anyone know a better way?

Best Answer

Apologies for the long answer (that might not be helpful or even an answer). The answer is so long since I am not sure what I am doing is anywhere near optimal and would love some feedback or to see what others are doing. I find the whole subs* access permissions to be extremely difficult and poorly documented by TMW. I don't know enough about other languages and OOP to know if is MATLAB specific or not. My solution is based upon using a metaclass object to determine the permissions (similar to what you hint at in your edit). The key to this solution is I limit my overloaded subs* methods to accessing only public properties and methods. I am comfortable with this since methods call the builtin subs* methods by default and you need to specifically code a call to the overloaded subs* methods. If you want your methods to be able to use the overloaded subs* methods to access private/protected properties and methods, you have two options. The first way is to extend the subs* methods to determine the access permissions of the caller. You can probably do this with dbstack and metaclass to figure out if the calling function has access to private and/or protected properties and methods. The second way is to write privatesubs* and protectedsubs* methods that have access permissions of private and protected, respectively. If a method has permission to access the protectedsubs* methods, then it also should have permission to access all protected properties and methods. Similarly, if a method has permission to access the privatesubs* methods, then it also should have permission to access all private and protected properties and methods. The first solution is easier for developers of subclasses since they only have to concern themselves with the subs* methods. I find the second easier to implement since I do not have to worry about determining the access permissions of the caller. Below is some slightly edited code for my actual implementation of the subsref method.
I start with defining a subsref method for my TopMost class. The TopMost class is not a child class of any other classes, but it is handle compatible (although I don't think that matters).
function varargout = subsref(Obj, S)
% Overloaded subsref

%











% varargout = subsref(Obj, S)

%
% This function overloads the builtin subsref for the TopMost class. It only allows access to
% public properties and methods. Access to private and protected methods is
% denied even if subsref is called from another method of the class. If you
% need to access a private or protected method via a subsref type call, you
% must implement your own method.
% Validate the number of arguments.





nRequiredArguments = 2;
nOptionalArguments = 0;
error(nargchk(nRequiredArguments, nRequiredArguments+nOptionalArguments, nargin, 'struct'));
% Overload the subsref method.

subsrefcheck(Obj, S);
[varargout{1:nargout}] = builtin('subsref', Obj, S);
end
The only thing this method does is check if the substruct object is "valid" (see the subsrefcheck function further below). The method hands everything off to the builtin subsref. The reason for this method is if you have the class hierarchy SubClass < ParentClass < TopMost, and SubClass overloads the subsref method, then I want the SubClass subsref method to use the ParentClass subsref method, and not the builtin subsref method, as the default. The problem is that MATLAB throws an error if the ParentClass does not have a subsref method (even though the ParentClass could use the builtin subsref method). By adding a subsref method to TopMost, I assure myself that ParentClass has a subsref method.
function varargout = subsref(Obj, S)
% Overloaded subsref
%
% varargout = subsref(Obj, S)
%
% This function overloads the ParentClass subsref method (which might be defined by the TopMost class). It only allows
% access to public properties and methods. Access to private and protected
% methods is denied even if subsref is called from another method of the
% class. If you need to access a private or protected method via a subsref
% type call, you must implement your own method.
% Validate the number of arguments.
nRequiredArguments = 2;
nOptionalArguments = 0;
error(nargchk(nRequiredArguments, nRequiredArguments+nOptionalArguments, nargin, 'struct'));
% Validate the arguments.




subsrefcheck(Obj, S);
% Overload the subsref method.
if strcmp(S(1).type, '.') && strcmp(S(1).subs, 'MyProp')
Value = Obj.MyPropSubsRefGet(S);
varargout = {Value};
else
[varargout{1:nargout}] = subsref@ParentClass(Obj, S); % This might actually jump all the way to subsref@Topmost(Obj, S);
end
end
Here the overloaded subsref method calls a special "get" method (MyPropSubsRefGet) for the property MyProp and passes all the other cases on to ParentClass.
Below is my subsrefcheck and the functions it depends on. I use these function in many of my classes, so I do not make them a method of my TopMost class, although I could.
function subsrefcheck(Obj, S)
% subsrefcheck checks if the substruct is valid for subsref
%
% subsrefcheck(Obj, S)
%
% Checks if the substruct S is valid for use with subsref on the object Obj.
% The substruct is not valid for the Obj if the substruct is not valid (see
% validatesubstruct). Further, if S accesses a property, the substruct is
% not valid if the get access of the property is not public. Finally, if S
% accesses a method, the substruct is not valid if the access of the method
% is not public. The function returns nothing if S is valid and throws the
% approariate error if S is not valid.
% Validate the number of arguments.
nRequiredArguments = 2;
nOptionalArguments = 0;
error(nargchk(nRequiredArguments, nRequiredArguments+nOptionalArguments, nargin, 'struct'));
% Validate the arguments.
validatesubstruct(S);
% Parse the substruct
if length(S) >= 1 && strcmp(S(1).type, '.')
SubsNameString = S(1).subs;
SubObj = Obj;
elseif length(S) >= 2 && strcmp(S(1).type, '()') && strcmp(S(2).type, '.')
SubsNameString = S(2).subs;
SubObj = Obj(S(1).subs{:});
else
return;
end
% Check if the property/method is public.
switch lower(gettype(Obj, SubsNameString))
case 'field'
case 'property'
[GetAccessString, SetAccessString] = getpropertyaccess(SubObj, SubsNameString); %#ok<NASGU>
if ~strcmp(GetAccessString, 'public')
throwAsCaller(MException('MATLAB:class:GetProhibited', ...
['Getting the ''', SubsNameString, ''' property of the ''', class(Obj), ''' class is not allowed.']));
end
case 'method'
AccessString = getmethodaccess(SubObj, SubsNameString);
if ~strcmp(AccessString, 'public')
throwAsCaller(MException( ...
'MATLAB:class:MethodRestricted', ...
['Cannot access method ''', SubsNameString, ''' in class ''', class(Obj), ''.']));
end
otherwise
throwAsCaller(MException('MATLAB:noSuchMethodOrField', ...
[ 'No appropriate method, property, or field ', SubsNameString, ' for class ', class(Obj), '.']));
end
end
function validatesubstruct(S)
% Checks if S is a valid substruct argument
%
% validatesubstruct(S)
%
% Returns nothing if S is a valid substruct (i.e., could have been generated
% by the substruct function) and throws the appropriate error if S is not a
% valid substruct.
% Validate the number of arguments.
nRequiredArguments = 1;
nOptionalArguments = 0;
error(nargchk(nRequiredArguments, nRequiredArguments+nOptionalArguments, nargin, 'struct'));
% Validate the arguments.
assert(isstruct(S), 'MATLAB:subsArgNotStruc', ...
'Subscript argument to SUBSREF and SUBSASGN must be a structure.');
assert(length(fieldnames(S)) == 2, 'MATLAB:subsMustHaveTwo', ...
'Subscript argument to SUBSREF and SUBSASGN must have two fields.');
assert(isequal(sort(fieldnames(S)), sort({'subs'; 'type'})), ...
'MATLAB:subsMustHaveTypeSubs', ['Subscript argument to SUBSREF ', ...
'and SUBSASGN must have two fields whose names are "type" ', ...
'and "subs".']);
assert(~isempty(S), 'MATLAB:subsArgEmpty', ...
'Subscript argument to SUBSREF and SUBSASGN must not be empty.');
assert(all(cellfun(@(x)(ischar(x) || iscell(x)), {S.subs})), ...
'MATLAB:subsSubsMustBeCellOrChar', ...
['The "subs" field for the subscript argument to SUBSREF ', ...
'and SUBSASGN must be a cell or character array.']);
assert(all(cellfun(@(x)ischar(x), {S.type})), ...
'MATLAB:subsTypeMustBeChar', ...
['The "type" field for the subscript argument to SUBSREF ', ...
'and SUBSASGN ', '\n', 'must be a character array.']);
assert(all(cellfun(@(x)any(strcmp(x, {'.'; '()'; '{}'})), {S.type})), ...
'MATLAB:subsTypeMustBeSquigglyOrSmooth', ...
['The "type" field for the subscript argument to SUBSREF ', ...
'and SUBSASGN ', '\n', 'must be a character array ', ...
'of "." or "{}" or "()".']);
assert(all(cellfun(@(x, y)(~strcmp(x, '.') || ~iscell(y) ...
|| ~isempty(y)), {S.type}, {S.subs})), ...
'MATLAB:subsCellIsEmpty', ...
['The "subs" field for the subscript argument to SUBSREF ', ...
'and SUBSASGN must be a non-empty cell or character array.']);
for iSub = 1:length(S)
assert(~strcmp(S(iSub).type, '()') || iscell(S(iSub).subs), ...
'MATLAB:subsSmoothTypeSubsMustBeCell', ...
'SUBS field must be a cell array for () TYPE.');
assert(~strcmp(S(iSub).type, '{}') || iscell(S(iSub).subs), ...
'MATLAB:subsSquigglyTypeSubsMustBeCell', ...
'SUBS field must be a cell array for {} TYPE.');
assert(~strcmp(S(iSub).type, '()') || (iSub == length(S) || ...
strcmp(S(iSub+1).type, '.')) , ...
'MATLAB:subsDotMustFollow', ...
'Only a dot field name can follow ()''s.');
end
end
function AccessString = getmethodaccess(Obj, MethodNameString)
% Gets the Access attribute of the method
%
% AccessString = getmethodaccess(Obj, MethodNameString)
%
% Uses the metaclass of the object Obj to determine the method
% MethodNameString access permission (private, protected, public) and
% returns the access permission as AccessString.
% Validate the number of arguments.
nRequiredArguments = 2;
nOptionalArguments = 0;
error(nargchk(nRequiredArguments, nRequiredArguments+nOptionalArguments, nargin, 'struct'));
% Validate the arguments.
assert(isobject(Obj), [mfilename, ':ArgumentCheck'], ...
['The Obj of class ''', class(Obj), ''' is not an object of a MATLAB class.']);
validateattributes(MethodNameString, {'char'}, {'row'}, [mfilename, ':ArgumentCheck'], 'MethodNameString');
MetaClassObj = metaclass(Obj);
NameList = {MetaClassObj.MethodList(:).Name};
iMethod = find(strcmp(MethodNameString, NameList), 1, 'first');
assert(~isempty(iMethod), [mfilename, ':ArgumentCheck'], ...
['''', MethodNameString, ''' is not a method of the ''', class(Obj), ''' class.']);
AccessString = MetaClassObj.MethodList(iMethod).Access;
end
function [GetAccessString, SetAccessString] = getpropertyaccess(Obj, PropertyNameString)
% Gets the GetAccess and SetAccess attributes of the property
%
% [GetAccessString, SetAccessString] = getpropertyaccess(Obj,
% PropertyNameString)
%
% Uses the metaclass of the object Obj to determine the property
% PropertyNameString get and set access permissions (private, protected,
% public) and returns the access permissions as GetAccessString,
% SetAccessString.
% Validate the number of arguments.
nRequiredArguments = 2;
nOptionalArguments = 0;
error(nargchk(nRequiredArguments, nRequiredArguments+nOptionalArguments, nargin, 'struct'));
% Validate the arguments.
assert(isobject(Obj), [mfilename, ':ArgumentCheck'], ...
['The Obj of class ''', class(Obj), ''' is not an object of a MATLAB class.']);
validateattributes(PropertyNameString, {'char'}, {'row'}, [mfilename, ':ArgumentCheck'], 'PropertyNameString');
MetaClassObj = metaclass(Obj);
NameList = {MetaClassObj.PropertyList(:).Name};
iProperty = find(strcmp(PropertyNameString, NameList), 1, 'first');
assert(~isempty(iProperty), [mfilename, ':ArgumentCheck'], ...
['''', PropertyNameString, ''' is not a property of the ''', class(Obj), ''' class.']);
GetAccessString = MetaClassObj.PropertyList(iProperty).GetAccess;
SetAccessString = MetaClassObj.PropertyList(iProperty).SetAccess;
end
I created the validate substruct function by trial and error. Basically I tried the builtin subsref function with every possible combination of arguments I could think of and recorded the errors. I really wish the substruct was a class. I might be being to restrictive on my substruct and reducing the power of subsref, but it is not causing me any problems. Also note that in newer versions of MATLAB the getmethodaccess and getpropertyaccess might be able to use isprop and ismethod. I am not sure what the performance implications of that change would be.