On 27.10.2018 11:19, Paul wrote:
> Below, I will post a question that I would like some help with (if possible).
> The best type of help would be something like "Think about using..." rather
> than just giving the answer. I would like to avoid using post-C++11 features
> if possible.
> People sometimes get (rightly) homework-suspicious when they see posts
> like these. This question is for self-study purposes only. I am not
> a student, and am not enrolled in any course. It is also not part of
> any external evaluation process or interview process.
> The source of the original question is
>
https://www.testdome.com/questions/cpp/multiple-choice-test/9808?visibility=1&skillId=7
>
> I have tried the very basic solution of using delete[] in the destructor
> without using smart pointers. That is not the intended solution.
> I tried using unique pointers with release(). This compiled but was also
> not the intended solution. reset() wasn't what they wanted either.
Not sure exactly what you're asking. Presumably that's explained at the
site you link to, maybe? Not looking there.
> Many thanks for your help,
>
> Paul
>
> // CODE BELOW
> /*Multiple choice test has several multiple choice questions. Each question can have only one correct answer. Additionally, timed multiple choice test can specify the time allowed for solving each question in the test.
>
> The code below satisfies this specification, but the customer complained that the memory usage of the program constantly increases. Fix this problem.*/
>
> #include <iostream>
> #include <string>
>
> class MultipleChoiceTest
> {
I like to place the member variables here, up front.
That way things become more concrete and easy to grasp.
> public:
> MultipleChoiceTest(int questionsCount)
> {
> this->questionsCount = questionsCount;
> answers = new int[questionsCount];
> for (int i = 0; i < questionsCount; i++)
> {
> answers[i] = -1;
> }
> }
Should use member initializers like so:
Multiple_choice_test( const int n_questions ):
m_answers( n_questions, -1 )
{}
... where `m_n_questions` is declared as a data member of type
`vector<int>`.
With use of raw arrays, like a student exercise with “don't use
`std::vector`!” requirement, it could look like this:
Multiple_choice_test( const int n_questions ):
m_n_questions( n_questions ),
m_answers( new int[n_questions] )
{
fill_n( m_answers, n_questions, -1 );
}
... where `fill_n` is `std::fill_n` from `<algorithm>`.
> void setAnswer(int questionIndex, int answer)
> {
> answers[questionIndex] = answer;
> }
Naming: consider using names that indicate the roles of the arguments in
a call, so as to make the /calls/ readable and clear and bug-free.
E.g. `set_answer_at` indicates that the first argument is an index.
Or you can go further and define appropriately named argument types like
struct Answer( int value; };
... then support /named arguments/ in the calls,
void set_at( int i, const Answer answer )
... which gives calls like
mct.set_at( 3, Answer{ 42 } );
Even better, as an alternative, make the raw array a raw array of `int`,
and just provide named collection indexing support,
auto answers( const int i )
-> Answer&
{ return m_answers[i]; }
... which gives calls like
mct.answers( 3 ) = 42;
... still clear as to the roles of `3` and `42`, but more concise.
I wouldn't go as far as to decouple the implementation from the
interface by letting `answers()` (or e.g. an `operator[]`) return a
proxy, but some fairly smart people have argued in that direction at
times. I think a class would have to be both much used and with the
clear possibility of a radical implementation change, for that amount of
decoupling overhead to make sense. But that's my view.
> int getAnswer(int questionIndex) const
> {
> return answers[questionIndex];
> }
Naming: in C++ prefix `get` for a simple getter or value-producing
function is usually just negative value verbosity. In e.g. Java it has a
value for use with tools that use introspection of the code; they
identify getter+setter pairs. C++ doesn't yet support introspection.
Consider: get_sin(x) + 2get_cos(x)
With the last option exemplified above, this getter would be
auto answers( const int i ) const
-> int
{ return m_answers[i]; }
> protected:
> int questionsCount;
Oh that's bad.
`protected`, granting direct access to derived classes, is seldom a good
choice. It's in the language for a reason, but. Instead of that access
level, an accessor for the value should be defined, like
auto n_questions() const
-> int
{ return m_questions; }
> private:
> int* answers;
> };
I'd put that at the start of the class, with implicit `private`
accessibility.
Notably lacking in this class: taking charge of cleanup and logical copying.
Cleanup is a destructor, logical copying is a copy constructor and
possibly a move constructor to support also moving.
Check out the original C++03 “rule of three”, the later C++11 “rule of
five” and the Good Robot's “rule of zero”.
Essentially, consider first using collection classes; if that doesn't
cut it use smart pointers (rule of zero, use types that already take
care of memory management for you); and if you absolutely must do things
manually, then define or declare all five of copy constructor, move
constructor, copy assignment, move assignment and destructor.
At the design level there is also a serious problem: that this class
directly models a general dynamic size array but is needlessly
restricted, via its naming, to use for multiple choice tests.
It would be better for general code reuse to define the dynamic length
array as a class template, e.g. called `Array_`, and then use that as a
data member in the implementation of `Multiple_choice_test`.
> class TimedMultipleChoiceTest : public MultipleChoiceTest
> {
> public:
> TimedMultipleChoiceTest(int questionsCount)
> : MultipleChoiceTest(questionsCount)
> {
> times = new int[questionsCount];
> for (int i = 0; i < questionsCount; i++)
> {
> times[i] = 0;
> }
> }
Even in C++03 one had value initialization. Zeroing by using a loop is
silly.
> void setTime(int questionIndex, int time)
> {
> times[questionIndex] = time;
> }
>
> int getTime(int questionIndex) const
> {
> return times[questionIndex];
> }
>
> private:
> int* times;
> };
The same problems as already mentioned for the first class.
Additional problem: with this derived class it's possible to set an
answer without setting the time for it. In particular it's possible to
update an answer without updating the associated time. So it models a
test where times /may/ be available for some or all answers, and where
it's entirely unclear what an associated time, if there is one, refers
to: original first answer, last update, or something else.
If the intent was that every answer should have a time, i.e. that this
class models an array of struct{ int answer; int time; }, then the
derived class relationship is wrong. It's an unnatural thing probably
stemming from a wish to reuse. The mentioned generalization of the first
class to an `Array_` class template would support more meaningful reuse.
Indeed, with the standard library an instance of the above is better
replaced with
struct Choice{ int value; };
struct Time{ int value; };
struct Answer{ Choice choice; Time time; };
const int n = ...;
vector<Answer> mct( n, Answer{ Choice{-1}, Time{0} } );
[snip]
Cheers & hth.,
- Alf