Problem
If I run this code with the value SIDE_NO = 1
it works perfectly, however when I change it to any other value, it gives me an error. What is happening?
Error
Not enough input arguments.
Error in CubePiece (line 15)
obj.posX = x;
Error in Main (line 21)
cubePieces(x,y) = piece;
Main method
SIDE_NO = 2;
PIECE_NO = 9;
cubeColors = zeros(SIDE_NO, PIECE_NO);
for s = 1: SIDE_NO
cimg = Snapshot();
simg = ScanFace(cimg);
cubeColors(s,:) = GetColours(simg);
end
for x = 1: SIDE_NO
for y = 1: PIECE_NO
piece = CubePiece(x,y,cubeColors(x,y));
cubePieces(x,y) = piece;
end
end
CubePiece
classdef CubePiece
properties
posX % 1, 2, 3
posY % 1, 2, 3
color % red, blue, orange, yellow, white, green
faceCode % R,L,U,D,F,B
faceNo % 1, 2, 3, 4, 5, 6
end
methods
function obj = CubePiece(x,y,col)
obj.posX = x;
obj.posY = y;
obj.color = col;
obj.faceNo = x;
obj.faceCode = obj.getFaceCode(x);
end
function code = getFaceCode(~,num)
if num == 1
code = 'R';
elseif num == 2
code = 'L';
elseif num == 3
code = 'U';
elseif num == 4
code = 'D';
elseif num == 5
code = 'F';
elseif num == 6
code = 'B';
end
end
end
end
The Problem
You issue is due to the fact that when you keep appending data to an array within a loop (causing the array to increase in size). MATLAB needs to attempt to expand the array to contain your data while at the same time ensuring that the array remains rectangular in shape.
So let's look a little closer at what's happening.
When you're looping with x
at 1 and incrementing y
by 1 each time. Each time through the loop you create a new CubePiece
instance. Then you implicitly add a new column to cubePieces
to store it.
cubePieces(x,y) = piece;
I emphasize implicit because you don't pre-allocate cubePieces
to be it's final size ([SIDE_NO x PIECE_NO]
). So if we stick the following statement within your loop, we can monitor the size of cubePieces
as it grows.
disp(size(cubePieces));
You will see the following entries as you loop through y
values while keeping x
equal to 1.
1 1
1 2
1 3
...
1 9
As you can see the number of columns continues to increase until we get to 9 (PIECE_NO
).
Now, when you get to x
= 2, MATLAB can't just add one more entry (as it has been doing), but rather it must add an entire row because the matrix/array can't be an irregular shape. So again if we watch the output of the disp
statement we will get.
2 9
2 9
...
2 9
If you notice, it's not changing size this time through the inner loop. This is because MATLAB filled in 8 "dummy" values for the entire row on the first pass through the loop since it requires that the new row have 9 columns but it only knows the value of the first one.
So the question is, How did MATLAB fill in that whole row of values if you didn't set them? The answer is that (for objects) MATLAB calls the default contstructor (the constructor called with no input arguments) for each of the elements it needs to completely fill the new row. This is the case because all members of an array must be the same type (ignoring heterogeneous objects).
So when you assign to cubePieces(2,1)
for the first time. What MATLAB actually does is this.
cubePieces(2,:) = [piece, CubePiece(), CubePiece(), CubePiece()...];
So it's these empty constructors that are giving you your issue. In your constructor, you don't check to ensure that the proper inputs were provided, but rather you just start trying to use them. This is obviously going to create an error about not enough input arguments.
CubePiece()
Not enough input arguments.
Error in CubePiece (line 13)
obj.posX = x;
13 obj.posX = x;
A Solution
What you want to do is gracefully handle the case where there are no inputs arguments to the contstructor and provide a valid "default" object.
You can do this by either returning without assigning any of the properties (if nargin == 0; return; end
). Or if you want default values for the various properties, you can specify those defaults in the properties
block of the class definition.
classdef CubePice
properties
posX = 1
posY = 1
color = 'red';
faceCode = 'R';
faceNo = 1
end
methods
function obj = CubePiece(x,y,col)
if nargin == 0
return;
end
obj.posX = x;
% Do other assignments
end
end
end
A Better Solution (Pre-allocation)
The better solution is to actually pre-allocate the variable that you are assigning to so that MATLAB isn't continually altering the size of it. This has some performance advantages. There are a few ways to do this
repmat
If you actually want a 2D array of CubePiece
objects you can actually pre-allocate your 2D array of CubePiece
objects to be the proper size. You can do this by using repmat
applied to a single instance of your class.
template = CubePiece(1,1,1);
cubePieces = repmat(template, [SIDE_NO, PIECE_NO]);
Now you will have a 2D array of objects (technically all handles to the same object) and MATLAB won't have to do any dynamic expansion of this array and try to guess what default values you want to use.
"Expand Once" approach
If you don't mind MATLAB calling the default constructor (and you've setup your class constructor to handle this) you could initialize this array by simply assigning the value at the largest row and column first which will expand the array to its maximum size at once.
cubePieces(SIDE_NO, PIECE_NO) = CubePiece(1,1,1);
size(cubePieces)
2 9
Cell Arrays
You could also store your objects within a cell array during creation.
cubePieces = cell(SIDE_NO, PIECE_NO);
for x = 1:size(cubePieces, 1)
for y = 1:size(cubePieces, 2)
cubePieces{x,y} = CubePiece(x, y, cubeColors(x,y))
end
end
Then afterwards if you want, you can use cell2mat
to convert the cell into a 2D array.
Examples Using Built-in DataTypes
Now this whole post has been talking about custom MATLAB classes; however, this really applies to all MATLAB built-in classes as well.
Take for example a numeric array of double
s.
x = 1:5
1 2 3 4 5
Now let's add an element at x(2,1)
.
x(2,1) = 6
1 2 3 4 5
6 0 0 0 0
Clearly, the default value of a double
is 0 because MATLAB filled in all the unspecified values with 0.
We can do the same for a cell array, where the default value is an empty array ([]
).
y = num2cell(1:5)
[1] [2] [3] [4] [5]
y{2,1} = 6;
[1] [2] [3] [4] [5]
[6] [] [] [] []
More information about this behavior can be found at this page.
Summary
When you are dynamically expanding a two-dimensional array of objects (whether they are built-in datatypes or custom classes), MATLAB has to fill in missing values in order to maintain a rectangular shape. The "default" value used to fill in the unspecified values depends on the datatype. For custom classes, this "default" datatype is created by calling the constructor with no arguments. This default behavior can be avoided by explicitly pre-allocating your multi-dimensional arrays.
Without going into the same, wonderful level of detail as Suever's answer, I'm just going to add a couple of practical solutions to the problem itself.
As Suever said, the problem is caused by the fact that you are adding a whole row of object that Matlab attempts to fill with the "default" value for the type. There are four alternative solutions to the conundrum, but the first two were already suggested in the other answer:
- Make your class gracefully handle the case (Suever's solution #1) by providing a default constructor. However, this might not be such a good idea because it might mess with your class invariants.
- Ensure that you preallocate the array using a default object of your choice (this was the answer involving
repmat
in the other answer).
Use a 1D array that can grow indefinitely, then reshape
it into the correct shape. This might be slow for large arrays and non-handle types, though, especially due to the constant reallocation.
for x = 1: SIDE_NO
for y = 1: PIECE_NO
i = sub2ind([SIDE_NO, PIECE_NO], x, y);
piece = CubePiece(x,y,cubeColors(x,y));
cubePieces(i) = piece; % Grow one by one
end
end
cubePieces = reshape(cubePieces, [SIDE_NO, PIECE_NO]);
Use a cell array instead of a class matrix. You can even have it preallocated to the right size, since the default value is the empty matrix. When you finish you can either use it as-is if it suits you, or use cell2mat to transform it into the right type array.
cubePieces = cell(SIDE_NO, PIECE_NO);
for x = 1: SIDE_NO
for y = 1: PIECE_NO
piece = CubePiece(x,y,cubeColors(x,y));
cubePieces{x,y} = piece;
end
end
cubePieces = cell2mat(cubePieces); % Or use as-is