If you focus on Java all the time it's easy to forget that something as simple as constructors and assignments follow very different paths in other languages.
Scala
Let's say we have a simple Scala class that takes no arguments in construction:class ClassWithNoArgs {
println("ClassWithNoArgs constructed")
def fn() = { println("ClassWithNoArgs.fn") }
}
def classWithNoArgs = new ClassWithNoArgs()
println("classWithNoArgs assigned. About to use...")
classWithNoArgs.fn()
So, a Java programmer might be surprised to see this output:
classWithNoArgs assigned. About to use...
ClassWithNoArgs constructed
ClassWithNoArgs.fn
That is, we make an assignment but not until we use the reference does instantiation take place!
This is entirely down to us using the def keyword rather than val. If the above code had been:
val classWithNoArgs = new ClassWithNoArgs()
.
.
Then the output would have been more familiar to a Java programmer.
Similarly, if we had a function that had side effects then returned 1, such as:
def fn() = {
println("calling something")
1
}
and two similar functions:
def callByValue(x: Int) = {
println("x1=" + x)
println("x2=" + x)
}
def callByName(x: => Int) = {
println("x1=" + x)
println("x2=" + x)
}
we could pass fn to both functions but in a call to the first, fn will be evaluated before the call and in the second, it will be evaluated twice - once upon each call to println.
Consequently, since both functions can take fn, we cannot overload functions with these two different argument lists. For instance, we could not have in the same namespace functions overloaded(x: Int) and overloaded(x: => Int).
C++
Let's take two very simple classes, one extending the other. First the base class that we will imaginatively call BaseClass:BaseClass.h
#include <iostream>
using namespace std;
class BaseClass {
public:
BaseClass();
BaseClass(string aString);
BaseClass(const BaseClass& orig);
virtual ~BaseClass();
const string aString;
};
#include "BaseClass.h"
BaseClass::BaseClass() {
cout << "BaseClass::BaseClass()\n";
}
BaseClass::BaseClass(string aString) : aString(aString) {
cout << "BaseClass::BaseClass(string)\n";
}
BaseClass::BaseClass(const BaseClass& orig) {
cout << "BaseClass::BaseClass(const BaseClass& orig)\n";
}
BaseClass::~BaseClass() {
cout << "BaseClass::~BaseClass()\n";
}
SubClass.h
#include <iostream>
#include "BaseClass.h"
using namespace std;
class SubClass : BaseClass {
public:
SubClass();
SubClass(string aString);
SubClass(const SubClass& orig);
virtual ~SubClass();
private:
};
#include "SubClass.h"
SubClass::SubClass() {
cout << "SubClass::SubClass()" << endl;
}
SubClass::SubClass(string aString) : BaseClass(aString) {
cout << "SubClass::SubClass(string aString)" << endl;
}
SubClass::SubClass(const SubClass& orig) {
cout << "SubClass::SubClass(const SubClass& orig)" << endl;
}
SubClass::~SubClass() {
cout << "SubClass::~SubClass()" << endl;
}
(My, isn't C++ verbose when you compare it to Scala?)
When we point to objects in C++, things are very similar to Java. But when we use code like this:
int main(int argc, char** argv) {
.
.
SubClass subclass; // instantiates a SubClass
SubClass otherSubclass = subclass;.
.
.
}
The output is very different.
BaseClass::BaseClass()
SubClass::SubClass()
BaseClass::BaseClass()
SubClass::SubClass(const SubClass& orig)
.
.
SubClass::~SubClass()
BaseClass::~BaseClass()
SubClass::~SubClass()
BaseClass::~BaseClass()
The last 4 lines are the destructors being called when we leave the method. But what of the first 4 lines?
The first two lines are fair enough. We're instantiating the SubClass via C++'s syntax for putting it on the stack. Just like Java, C++ put's an implicit call to super in the constructor for us automatically.
The third and fourth are the super constructor (again) and the copy constructor that assigns subclass to otherSubclass. This is very much unlike Java where there would be two references to the same object but here we have two different objects. We can prove this by writing a CppUnit test that looks like this:
SubClass subClass;
SubClass otherSubClass = subClass;
CPPUNIT_ASSERT(&subClass != &otherSubClass);
Implicitly, C++ is passing otherSubclass to the copy constructor and it's the job of the developer to populate his new object.
Now, let's instantiate another SubClass but this time with an argument:
string aString = "a string";
SubClass subClassWArg = SubClass(aString);
CPPUNIT_ASSERT(aString == subClassWArg.aString); // must have Subclass : public BaseClass to access this
SubClass otherWArg = subClassWArg;
CPPUNIT_ASSERT(otherWArg.aString == subClassWArg.aString);
Hmm, this fails. So, let's change the copy constructor thus:
SubClass::SubClass(const SubClass& orig) : BaseClass(orig.aString) {
.
.
And that passes.
Note, you usually do assignments in C++ with:
this->aString = orig.aString;
but in our code, we made aString a constant so this would cause a compilation error. Note, by making it constant, we must write our own copy-constructor and not use the compiler implicitly generates for us (see here for why). This demonstrates the difference between initialization (the former) and assignment (the latter).
No comments:
Post a Comment