I'm trying to develop a data structure for a mesh using the MATLAB OOP functionalities. Long story short, I'm modifying a field from an instance that inherits from the same base class as another instance, and both instances are being modified, as if the field was declared static!
I have this code inside an abstract base class (m_element) in MATLAB:
properties(Access = protected)
nodes = containers.Map('KeyType','int64', 'ValueType', 'any');
faces = containers.Map('KeyType','int64', 'ValueType', 'any');
end
These field represent the connectivity of each element. For example, which nodes are neighbors of the n'th node, or which faces are adjacent to the n'th node.
I also have two other classes: m_face and m_node, each one inheriting from m_element. m_node is very simple:
classdef m_node < m_element
properties
x = 0;
y = 0;
z = 0;
end
methods
function node = m_node(gmsh_id, x, y, z)
node = node@m_element(gmsh_id);
node.x = x;
node.y = y;
node.z = z;
end
end
end
But when it comes to m_face, I'm facing an issue. Here's the constructor, where the problem is arising:
function face = m_face(varargin)
face = face@m_element(varargin{1});
for k = 2:nargin
nod = varargin{k};
if(~isa(nod, 'm_node'))
error('Algum dos argumentos não é um node!');
elseif (~isvalid(nod))
error('Algum dos argumentos não é válido!');
else
face.nodes(nod.gmsh_id) = nod;
nod.faces(face.gmsh_id) = face;
end
end
end
The m_face constructor expects the face ID to come as the first argument, and the rest should be the nodes that forms the face. The line face.nodes(nod.gmsh_id) = nod;
seems to be causing my problem. I have a m_mesh class which shall hold every node and face:
classdef m_mesh < handle
properties(SetAccess = private)
nodes = containers.Map('KeyType','int64', 'ValueType', 'any');
faces = containers.Map('KeyType','int64', 'ValueType', 'any');
end
methods
function theMesh = m_mesh(msh)
for idx = 1:numel(msh.POS(:,1))
n = msh.POS(idx,:);
theMesh.nodes(idx) = m_node(idx, n(1), n(2), n(3));
end
for idx = 1:numel(msh.TRIANGLES(:,1))
ele = msh.TRIANGLES(idx,:);
nod(1) = theMesh.nodes(ele(1));
nod(2) = theMesh.nodes(ele(2));
nod(3) = theMesh.nodes(ele(3));
theMesh.faces(idx) = m_face(idx, nod(1), nod(2), nod(3));
end
end
end
end
The msh
argument to the constructor holds the nodes spatial positions, and also the nodes that composes each face (which in this case are triangles).
Here's what I get when I build the mesh:
>> mesh = m_mesh(m)
mesh =
m_mesh with properties:
nodes: [5x1 containers.Map]
edges: [0x1 containers.Map]
faces: [4x1 containers.Map]
>> nod = mesh.nodes.values();
>> nod{1}.i_nodes
ans =
[1x1 m_node] [1x1 m_node] [1x1 m_node] [1x1 m_node] [1x1 m_node]
The i_nodes returns the instance map values. Now, how's that possible? Why does my first (and all the others!) node have FIVE adjacent nodes, if I haven't set this yet? Why should MATLAB change an non-static field for all instances and all subclasses when I access this field from a random instance?
You should initialise your
nodes
andfaces
properties in the constructor, not as a default property value.So what's going on here? The first thing to note is that
containers.Map
is ahandle
class.Normal variables in MATLAB have value behaviour:
Note that
b
has not changed when you changeda
- it is a copy ofa
, anda
andb
are value variables that have pass-by-value behaviour.Some other variables have handle behaviour:
Note that
b
has changed when you changeda
- it is a reference toa
, anda
andb
are handle variables that have pass-by-reference behaviour.containers.Map
variables are handle variables.The second thing to note is that property default values are evaluated once, at the first time the class is instantiated. (If you clear the class definition with
clear classes
, it will be evaluated again subsequently when you next instantiate it). They are not evaluated every time you create an object.So what's happening is that the first time you create an object, the class is instantiated, the
containers.Map
is evaluated, and each object after that is getting the samecontainers.Map
as its property. Since it's a handle variable, changes that you then make to the property in one object are referenced in the properties of others.You don't want that: instead, you should initialise the values in the class constructor. Then it will get evaluated separately each time you construct an object, and each will get a separate one.
This behaviour (i.e., when you have handle variables as property defaults) can be confusing, but I think it's the right behaviour - it is documented, although I think it could be more clearly signposted. Here's an article where the topic is discussed - in the comments, the issue is debated between the author, myself, and a MathWorks developer responsible for OO syntax and design.