Type-safe Euclidean vectors in C++











up vote
0
down vote

favorite












I'm trying to follow the Raytracing in One Weekend book, and I found that he uses the same Vec3 class for everything - colors, coordinates, direction, and more:



struct Vec3 { float x, y, z };


He also overloads every possible operator of the Vec3 class to make it convenient to use, and adds plenty of member functions (length, distance_to, etc). While this definitely reduces the amount of code one has to write, it isn't terribly safe, and allows for things doesn't make sense (why would you want a distance_to between two Colors?). I was trying to fix that.



I defined a base Vector type:



struct Vector {
Vector(float, float, float);

float x;
float y;
float z;

float length() const;
};


According to the Wikipedia page, there can be 2 types of vectors - bound vectors (goes from point A to point B) and free vectors (the particular point is of no significance, only the magnitude and direction are). I defined these types too, but with very limited operators.



Notice how you can add a FreeVector to a BoundVector to give a BoundVector, but you cannot add 2 BoundVectors:



struct BoundVector : public Vector {
BoundVector(float, float, float);

BoundVector& operator+=(const FreeVector&);
BoundVector& operator-=(const FreeVector&);

FreeVector operator-(const BoundVector&) const;
};

struct FreeVector : public Vector {
FreeVector(float, float, float);
explicit FreeVector(const UnitVector&);

FreeVector& operator+=(const FreeVector&);
FreeVector& operator-=(const FreeVector&);

FreeVector& operator*=(float);
FreeVector& operator/=(float);

UnitVector unit() const;
float dot(const FreeVector&) const;
};

BoundVector operator+(BoundVector, const FreeVector&);
BoundVector operator-(BoundVector, const FreeVector&);

FreeVector operator+(FreeVector, const FreeVector&);
FreeVector operator-(FreeVector, const FreeVector&);

FreeVector operator*(FreeVector, float);
FreeVector operator/(FreeVector, float);


I would have liked the UnitVector class to extend the FreeVector (or even the Vector) class, but I can't, since the implementation is completely different. For starters, to ensure that it is guaranteed to be a unit vector (x*x + y*y + z*z == 1) I have to make all members const:



struct UnitVector {
UnitVector(float, float, float);
explicit UnitVector(const FreeVector&);

const float x;
const float y;
const float z;

FreeVector operator*(float) const;
FreeVector operator/(float) const;

private:
UnitVector(float, float, float, float);
};

UnitVector::UnitVector(const float x, const float y, const float z)
: UnitVector(x, y, z, std::sqrt(x * x + y * y + z * z)) {}

UnitVector::UnitVector(const FreeVector& v) : UnitVector(v.x, v.y, v.z) {}

FreeVector UnitVector::operator*(const float k) const {
return FreeVector(x * k, y * k, z * k);
}

FreeVector UnitVector::operator/(const float k) const {
return FreeVector(x / k, y / k, z / k);
}

UnitVector::UnitVector(const float x, const float y, const float z, const float r)
: x{x / r}, y{y / r}, z{z / r} {}


UnitVectors allow you to define directions in a much better manner:



struct Ray {
BoundVector source;
UnitVector direction;

BoundVector parametric_eq(float) const;
};

BoundVector Ray::parametric_eq(const float t) const {
return source + direction * t;
}


However, this is not all sunshine and roses, as it sometimes results in very ugly-looking static_casts:



struct Lambertian : public Material {
FreeVector albedo;
std::optional<Scatter> scatter(const Ray&, const Strike&) const override;
};

FreeVector random_in_unit_sphere() {
std::random_device r;
std::default_random_engine gen(r());
std::uniform_real_distribution<float> distribution(0, 1);

while (true) {
const FreeVector v(distribution(gen), distribution(gen), distribution(gen));
if (v.length() < 1) return v;
}
}

std::optional<Scatter> Lambertian::scatter(const Ray& ray,
const Strike& strike) const {
return Scatter{.attenuation = albedo,
.scattered = Ray{.source = strike.point,
.direction = static_cast<UnitVector>(
static_cast<FreeVector>(strike.normal) +
random_in_unit_sphere())}};
}


Note that the std::optional is added here because the material may choose to absorb the Ray completely with some probability, and hence not scatter it at all.



Is there a way to reduce the number of static_casts in the last example (or at least the overhead due to them)?



Any other feedback, comments and nitpickings are also welcomed.










share|improve this question







New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.




















  • designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
    – papagaga
    25 mins ago










  • Strangely, CMake didn't support it so I couldn't use set(CMAKE_CXX_STANDARD 2a), but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a) to the CMakeLists.txt file.
    – ajeetdsouza
    20 mins ago















up vote
0
down vote

favorite












I'm trying to follow the Raytracing in One Weekend book, and I found that he uses the same Vec3 class for everything - colors, coordinates, direction, and more:



struct Vec3 { float x, y, z };


He also overloads every possible operator of the Vec3 class to make it convenient to use, and adds plenty of member functions (length, distance_to, etc). While this definitely reduces the amount of code one has to write, it isn't terribly safe, and allows for things doesn't make sense (why would you want a distance_to between two Colors?). I was trying to fix that.



I defined a base Vector type:



struct Vector {
Vector(float, float, float);

float x;
float y;
float z;

float length() const;
};


According to the Wikipedia page, there can be 2 types of vectors - bound vectors (goes from point A to point B) and free vectors (the particular point is of no significance, only the magnitude and direction are). I defined these types too, but with very limited operators.



Notice how you can add a FreeVector to a BoundVector to give a BoundVector, but you cannot add 2 BoundVectors:



struct BoundVector : public Vector {
BoundVector(float, float, float);

BoundVector& operator+=(const FreeVector&);
BoundVector& operator-=(const FreeVector&);

FreeVector operator-(const BoundVector&) const;
};

struct FreeVector : public Vector {
FreeVector(float, float, float);
explicit FreeVector(const UnitVector&);

FreeVector& operator+=(const FreeVector&);
FreeVector& operator-=(const FreeVector&);

FreeVector& operator*=(float);
FreeVector& operator/=(float);

UnitVector unit() const;
float dot(const FreeVector&) const;
};

BoundVector operator+(BoundVector, const FreeVector&);
BoundVector operator-(BoundVector, const FreeVector&);

FreeVector operator+(FreeVector, const FreeVector&);
FreeVector operator-(FreeVector, const FreeVector&);

FreeVector operator*(FreeVector, float);
FreeVector operator/(FreeVector, float);


I would have liked the UnitVector class to extend the FreeVector (or even the Vector) class, but I can't, since the implementation is completely different. For starters, to ensure that it is guaranteed to be a unit vector (x*x + y*y + z*z == 1) I have to make all members const:



struct UnitVector {
UnitVector(float, float, float);
explicit UnitVector(const FreeVector&);

const float x;
const float y;
const float z;

FreeVector operator*(float) const;
FreeVector operator/(float) const;

private:
UnitVector(float, float, float, float);
};

UnitVector::UnitVector(const float x, const float y, const float z)
: UnitVector(x, y, z, std::sqrt(x * x + y * y + z * z)) {}

UnitVector::UnitVector(const FreeVector& v) : UnitVector(v.x, v.y, v.z) {}

FreeVector UnitVector::operator*(const float k) const {
return FreeVector(x * k, y * k, z * k);
}

FreeVector UnitVector::operator/(const float k) const {
return FreeVector(x / k, y / k, z / k);
}

UnitVector::UnitVector(const float x, const float y, const float z, const float r)
: x{x / r}, y{y / r}, z{z / r} {}


UnitVectors allow you to define directions in a much better manner:



struct Ray {
BoundVector source;
UnitVector direction;

BoundVector parametric_eq(float) const;
};

BoundVector Ray::parametric_eq(const float t) const {
return source + direction * t;
}


However, this is not all sunshine and roses, as it sometimes results in very ugly-looking static_casts:



struct Lambertian : public Material {
FreeVector albedo;
std::optional<Scatter> scatter(const Ray&, const Strike&) const override;
};

FreeVector random_in_unit_sphere() {
std::random_device r;
std::default_random_engine gen(r());
std::uniform_real_distribution<float> distribution(0, 1);

while (true) {
const FreeVector v(distribution(gen), distribution(gen), distribution(gen));
if (v.length() < 1) return v;
}
}

std::optional<Scatter> Lambertian::scatter(const Ray& ray,
const Strike& strike) const {
return Scatter{.attenuation = albedo,
.scattered = Ray{.source = strike.point,
.direction = static_cast<UnitVector>(
static_cast<FreeVector>(strike.normal) +
random_in_unit_sphere())}};
}


Note that the std::optional is added here because the material may choose to absorb the Ray completely with some probability, and hence not scatter it at all.



Is there a way to reduce the number of static_casts in the last example (or at least the overhead due to them)?



Any other feedback, comments and nitpickings are also welcomed.










share|improve this question







New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.




















  • designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
    – papagaga
    25 mins ago










  • Strangely, CMake didn't support it so I couldn't use set(CMAKE_CXX_STANDARD 2a), but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a) to the CMakeLists.txt file.
    – ajeetdsouza
    20 mins ago













up vote
0
down vote

favorite









up vote
0
down vote

favorite











I'm trying to follow the Raytracing in One Weekend book, and I found that he uses the same Vec3 class for everything - colors, coordinates, direction, and more:



struct Vec3 { float x, y, z };


He also overloads every possible operator of the Vec3 class to make it convenient to use, and adds plenty of member functions (length, distance_to, etc). While this definitely reduces the amount of code one has to write, it isn't terribly safe, and allows for things doesn't make sense (why would you want a distance_to between two Colors?). I was trying to fix that.



I defined a base Vector type:



struct Vector {
Vector(float, float, float);

float x;
float y;
float z;

float length() const;
};


According to the Wikipedia page, there can be 2 types of vectors - bound vectors (goes from point A to point B) and free vectors (the particular point is of no significance, only the magnitude and direction are). I defined these types too, but with very limited operators.



Notice how you can add a FreeVector to a BoundVector to give a BoundVector, but you cannot add 2 BoundVectors:



struct BoundVector : public Vector {
BoundVector(float, float, float);

BoundVector& operator+=(const FreeVector&);
BoundVector& operator-=(const FreeVector&);

FreeVector operator-(const BoundVector&) const;
};

struct FreeVector : public Vector {
FreeVector(float, float, float);
explicit FreeVector(const UnitVector&);

FreeVector& operator+=(const FreeVector&);
FreeVector& operator-=(const FreeVector&);

FreeVector& operator*=(float);
FreeVector& operator/=(float);

UnitVector unit() const;
float dot(const FreeVector&) const;
};

BoundVector operator+(BoundVector, const FreeVector&);
BoundVector operator-(BoundVector, const FreeVector&);

FreeVector operator+(FreeVector, const FreeVector&);
FreeVector operator-(FreeVector, const FreeVector&);

FreeVector operator*(FreeVector, float);
FreeVector operator/(FreeVector, float);


I would have liked the UnitVector class to extend the FreeVector (or even the Vector) class, but I can't, since the implementation is completely different. For starters, to ensure that it is guaranteed to be a unit vector (x*x + y*y + z*z == 1) I have to make all members const:



struct UnitVector {
UnitVector(float, float, float);
explicit UnitVector(const FreeVector&);

const float x;
const float y;
const float z;

FreeVector operator*(float) const;
FreeVector operator/(float) const;

private:
UnitVector(float, float, float, float);
};

UnitVector::UnitVector(const float x, const float y, const float z)
: UnitVector(x, y, z, std::sqrt(x * x + y * y + z * z)) {}

UnitVector::UnitVector(const FreeVector& v) : UnitVector(v.x, v.y, v.z) {}

FreeVector UnitVector::operator*(const float k) const {
return FreeVector(x * k, y * k, z * k);
}

FreeVector UnitVector::operator/(const float k) const {
return FreeVector(x / k, y / k, z / k);
}

UnitVector::UnitVector(const float x, const float y, const float z, const float r)
: x{x / r}, y{y / r}, z{z / r} {}


UnitVectors allow you to define directions in a much better manner:



struct Ray {
BoundVector source;
UnitVector direction;

BoundVector parametric_eq(float) const;
};

BoundVector Ray::parametric_eq(const float t) const {
return source + direction * t;
}


However, this is not all sunshine and roses, as it sometimes results in very ugly-looking static_casts:



struct Lambertian : public Material {
FreeVector albedo;
std::optional<Scatter> scatter(const Ray&, const Strike&) const override;
};

FreeVector random_in_unit_sphere() {
std::random_device r;
std::default_random_engine gen(r());
std::uniform_real_distribution<float> distribution(0, 1);

while (true) {
const FreeVector v(distribution(gen), distribution(gen), distribution(gen));
if (v.length() < 1) return v;
}
}

std::optional<Scatter> Lambertian::scatter(const Ray& ray,
const Strike& strike) const {
return Scatter{.attenuation = albedo,
.scattered = Ray{.source = strike.point,
.direction = static_cast<UnitVector>(
static_cast<FreeVector>(strike.normal) +
random_in_unit_sphere())}};
}


Note that the std::optional is added here because the material may choose to absorb the Ray completely with some probability, and hence not scatter it at all.



Is there a way to reduce the number of static_casts in the last example (or at least the overhead due to them)?



Any other feedback, comments and nitpickings are also welcomed.










share|improve this question







New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.











I'm trying to follow the Raytracing in One Weekend book, and I found that he uses the same Vec3 class for everything - colors, coordinates, direction, and more:



struct Vec3 { float x, y, z };


He also overloads every possible operator of the Vec3 class to make it convenient to use, and adds plenty of member functions (length, distance_to, etc). While this definitely reduces the amount of code one has to write, it isn't terribly safe, and allows for things doesn't make sense (why would you want a distance_to between two Colors?). I was trying to fix that.



I defined a base Vector type:



struct Vector {
Vector(float, float, float);

float x;
float y;
float z;

float length() const;
};


According to the Wikipedia page, there can be 2 types of vectors - bound vectors (goes from point A to point B) and free vectors (the particular point is of no significance, only the magnitude and direction are). I defined these types too, but with very limited operators.



Notice how you can add a FreeVector to a BoundVector to give a BoundVector, but you cannot add 2 BoundVectors:



struct BoundVector : public Vector {
BoundVector(float, float, float);

BoundVector& operator+=(const FreeVector&);
BoundVector& operator-=(const FreeVector&);

FreeVector operator-(const BoundVector&) const;
};

struct FreeVector : public Vector {
FreeVector(float, float, float);
explicit FreeVector(const UnitVector&);

FreeVector& operator+=(const FreeVector&);
FreeVector& operator-=(const FreeVector&);

FreeVector& operator*=(float);
FreeVector& operator/=(float);

UnitVector unit() const;
float dot(const FreeVector&) const;
};

BoundVector operator+(BoundVector, const FreeVector&);
BoundVector operator-(BoundVector, const FreeVector&);

FreeVector operator+(FreeVector, const FreeVector&);
FreeVector operator-(FreeVector, const FreeVector&);

FreeVector operator*(FreeVector, float);
FreeVector operator/(FreeVector, float);


I would have liked the UnitVector class to extend the FreeVector (or even the Vector) class, but I can't, since the implementation is completely different. For starters, to ensure that it is guaranteed to be a unit vector (x*x + y*y + z*z == 1) I have to make all members const:



struct UnitVector {
UnitVector(float, float, float);
explicit UnitVector(const FreeVector&);

const float x;
const float y;
const float z;

FreeVector operator*(float) const;
FreeVector operator/(float) const;

private:
UnitVector(float, float, float, float);
};

UnitVector::UnitVector(const float x, const float y, const float z)
: UnitVector(x, y, z, std::sqrt(x * x + y * y + z * z)) {}

UnitVector::UnitVector(const FreeVector& v) : UnitVector(v.x, v.y, v.z) {}

FreeVector UnitVector::operator*(const float k) const {
return FreeVector(x * k, y * k, z * k);
}

FreeVector UnitVector::operator/(const float k) const {
return FreeVector(x / k, y / k, z / k);
}

UnitVector::UnitVector(const float x, const float y, const float z, const float r)
: x{x / r}, y{y / r}, z{z / r} {}


UnitVectors allow you to define directions in a much better manner:



struct Ray {
BoundVector source;
UnitVector direction;

BoundVector parametric_eq(float) const;
};

BoundVector Ray::parametric_eq(const float t) const {
return source + direction * t;
}


However, this is not all sunshine and roses, as it sometimes results in very ugly-looking static_casts:



struct Lambertian : public Material {
FreeVector albedo;
std::optional<Scatter> scatter(const Ray&, const Strike&) const override;
};

FreeVector random_in_unit_sphere() {
std::random_device r;
std::default_random_engine gen(r());
std::uniform_real_distribution<float> distribution(0, 1);

while (true) {
const FreeVector v(distribution(gen), distribution(gen), distribution(gen));
if (v.length() < 1) return v;
}
}

std::optional<Scatter> Lambertian::scatter(const Ray& ray,
const Strike& strike) const {
return Scatter{.attenuation = albedo,
.scattered = Ray{.source = strike.point,
.direction = static_cast<UnitVector>(
static_cast<FreeVector>(strike.normal) +
random_in_unit_sphere())}};
}


Note that the std::optional is added here because the material may choose to absorb the Ray completely with some probability, and hence not scatter it at all.



Is there a way to reduce the number of static_casts in the last example (or at least the overhead due to them)?



Any other feedback, comments and nitpickings are also welcomed.







c++ coordinate-system raytracing






share|improve this question







New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.











share|improve this question







New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.









share|improve this question




share|improve this question






New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.









asked 30 mins ago









ajeetdsouza

11




11




New contributor




ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.





New contributor





ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.






ajeetdsouza is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.












  • designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
    – papagaga
    25 mins ago










  • Strangely, CMake didn't support it so I couldn't use set(CMAKE_CXX_STANDARD 2a), but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a) to the CMakeLists.txt file.
    – ajeetdsouza
    20 mins ago


















  • designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
    – papagaga
    25 mins ago










  • Strangely, CMake didn't support it so I couldn't use set(CMAKE_CXX_STANDARD 2a), but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a) to the CMakeLists.txt file.
    – ajeetdsouza
    20 mins ago
















designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago




designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago












Strangely, CMake didn't support it so I couldn't use set(CMAKE_CXX_STANDARD 2a), but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a) to the CMakeLists.txt file.
– ajeetdsouza
20 mins ago




Strangely, CMake didn't support it so I couldn't use set(CMAKE_CXX_STANDARD 2a), but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a) to the CMakeLists.txt file.
– ajeetdsouza
20 mins ago















active

oldest

votes











Your Answer





StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");

StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});






ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.










 

draft saved


draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f208317%2ftype-safe-euclidean-vectors-in-c%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown






























active

oldest

votes













active

oldest

votes









active

oldest

votes






active

oldest

votes








ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.










 

draft saved


draft discarded


















ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.













ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.












ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.















 


draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f208317%2ftype-safe-euclidean-vectors-in-c%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

404 Error Contact Form 7 ajax form submitting

How to know if a Active Directory user can login interactively

TypeError: fit_transform() missing 1 required positional argument: 'X'