Where are the pointers supposed to be? - c++

Unfamiliar with C++ programming,
Trying to conceptualize a problem here:
In this exercise we’ll confine ourselves to one numerical type,
float, so we’ll need an array of this type; call it fmemory. However, pointer values
(addresses) are also stored in memory, so we’ll need another array to store them. Since
we’re using array indexes to model addresses, and indexes for all but the largest arrays
can be stored in type int, we’ll create an array of this type (call it pmemory) to hold these
“pointers.”
An index to fmemory (call it fmem_top) points to the next available place where a float
value can be stored. There’s a similar index to pmemory (call it pmem_top). Don’t worry
about running out of “memory.” We’ll assume these arrays are big enough so that each
time we store something we can simply insert it at the next index number in the array.
Other than this, we won’t worry about memory management.
Create a class called Float. We’ll use it to model numbers of type float that are stored
in fmemory instead of real memory. The only instance data in Float is its own “address”;
that is, the index where its float value is stored in fmemory. Call this instance variable
addr. Class Float also needs two member functions. The first is a one-argument constructor
to initialize the Float with a float value. This constructor stores the float
value in the element of fmemory pointed to by fmem_top, and stores the value of
fmem_top in addr. This is similar to how the compiler and linker arrange to store an ordinary
variable in real memory. The second member function is the overloaded & operator.
It simply returns the pointer (really the index, type int) value in addr.
So, what I have deducted was that I need to create something like this
#include <iostream>
using namespace std;
class Float
{
private:
int addr;
float fmem_top,pmem_top;
public:
Float(float* fmem_top){};
Float(int* addr){}
};
int main()
{
//this is where I become confused
}
Would I use something like this in the main method?
Float fmem;
Float pmem;

In this exercise we’ll confine ourselves to one numerical type, float, so we’ll need an array of this type; call it fmemory. However, pointer values (addresses) are also stored in memory, so we’ll need another array to store them. Since we’re using array indexes to model addresses, and indexes for all but the largest arrays can be stored in type int, we’ll create an array of this type (call it pmemory) to hold these “pointers.”
float fmemory[N]; // "we'll need an array of [float]"
int pmemory[N]; // "we'll create an array... pmemory"
An index to fmemory (call it fmem_top) points to the next available place where a float value can be stored. There’s a similar index to pmemory (call it pmem_top). Don’t worry about running out of “memory.” We’ll assume these arrays are big enough so that each time we store something we can simply insert it at the next index number in the array. Other than this, we won’t worry about memory management.
int fmem_top = 0; // "next available place..." fmemory[fmem_top]
int pmem_top = 0; // "similar index to pmemory"
Create a class called Float. We’ll use it to model numbers of type float that are stored in fmemory instead of real memory. The only instance data in Float is its own “address”; that is, the index where its float value is stored in fmemory. Call this instance variable addr.
class Float
{
int addr;
class Float also needs two member functions. The first is a one-argument constructor to initialize the Float with a float value. This constructor stores the float value in the element of fmemory pointed to by fmem_top, and stores the value of fmem_top in addr.
Float(float f)
{
fmemory[fmem_top] = f;
addr = fmem_top;
++fmem_top;
}
This is similar to how the compiler and linker arrange to store an ordinary variable in real memory.
Yeah sure.
The second member function is the overloaded & operator. It simply returns the pointer (really the index, type int) value in addr.
int operator&() { return addr; }
Discussion
There's no indication of the intended use of pmemory, so it's unclear what ought to be done with it. It doesn't make a lot of sense really.
Overall, the interface for Float doesn't provide any clean abstraction for the code using it: the stored values' indices can be retrieved with &, but the caller still needs to know about fmemory to find the actual float value.
I hope the course improves....

Try this. I think the exercise was to abstract for beginners. (I didn't compile this though, it's more for you to understand the concept.)
const int NUM_FLOATS_MAX = 1000;
class Float
{
static float floatArr[];
static int fmem_top;
int addr;
public:
int operator&() { return addr; }
Float(float f) { floatArr[fmem_top] = f; addr = fmem_top; fmem_top++; }
}
// static members need to be defined outside the class declaration
float Float::floatArr[NUM_FLOATS_MAX];
int Float::fmem_top = 0;
// should print 0 and 1, the indices. (How we get the float value
// we don't know -- overload the operator*()?
int main()
{
Float f1 = 1, f2 = 2;
cout << &f1 << endl << &f2 << endl;
}

Related

Why can't we use a void* to operate on the object it addresses

I am learning C++ using C++ Primer 5th edition. In particular, i read about void*. There it is written that:
We cannot use a void* to operate on the object it addresses—we don’t know that object’s type, and the type determines what operations we can perform on that object.
void*: Pointer type that can point to any nonconst type. Such pointers may not
be dereferenced.
My question is that if we're not allowed to use a void* to operate on the object it addressess then why do we need a void*. Also, i am not sure if the above quoted statement from C++ Primer is technically correct because i am not able to understand what it is conveying. Maybe some examples can help me understand what the author meant when he said that "we cannot use a void* to operate on the object it addresses". So can someone please provide some example to clarify what the author meant and whether he is correct or incorrect in saying the above statement.
My question is that if we're not allowed to use a void* to operate on the object it addressess then why do we need a void*
It's indeed quite rare to need void* in C++. It's more common in C.
But where it's useful is type-erasure. For example, try to store an object of any type in a variable, determining the type at runtime. You'll find that hiding the type becomes essential to achieve that task.
What you may be missing is that it is possible to convert the void* back to the typed pointer afterwards (or in special cases, you can reinterpret as another pointer type), which allows you to operate on the object.
Maybe some examples can help me understand what the author meant when he said that "we cannot use a void* to operate on the object it addresses"
Example:
int i;
int* int_ptr = &i;
void* void_ptr = &i;
*int_ptr = 42; // OK
*void_ptr = 42; // ill-formed
As the example demonstrates, we cannot modify the pointed int object through the pointer to void.
so since a void* has no size(as written in the answer by PMF)
Their answer is misleading or you've misunderstood. The pointer has a size. But since there is no information about the type of the pointed object, the size of the pointed object is unknown. In a way, that's part of why it can point to an object of any size.
so how can a int* on the right hand side be implicitly converted to a void*
All pointers to objects can implicitly be converted to void* because the language rules say so.
Yes, the author is right.
A pointer of type void* cannot be dereferenced, because it has no size1. The compiler would not know how much data he needs to get from that address if you try to access it:
void* myData = std::malloc(1000); // Allocate some memory (note that the return type of malloc() is void*)
int value = *myData; // Error, can't dereference
int field = myData->myField; // Error, a void pointer obviously has no fields
The first example fails because the compiler doesn't know how much data to get. We need to tell it the size of the data to get:
int value = *(int*)myData; // Now fine, we have casted the pointer to int*
int value = *(char*)myData; // Fine too, but NOT the same as above!
or, to be more in the C++-world:
int value = *static_cast<int*>(myData);
int value = *static_cast<char*>(myData);
The two examples return a different result, because the first gets an integer (32 bit on most systems) from the target address, while the second only gets a single byte and then moves that to a larger variable.
The reason why the use of void* is sometimes still useful is when the type of data doesn't matter much, like when just copying stuff around. Methods such as memset or memcpy take void* parameters, since they don't care about the actual structure of the data (but they need to be given the size explicitly). When working in C++ (as opposed to C) you'll not use these very often, though.
1 "No size" applies to the size of the destination object, not the size of the variable containing the pointer. sizeof(void*) is perfectly valid and returns, the size of a pointer variable. This is always equal to any other pointer size, so sizeof(void*)==sizeof(int*)==sizeof(MyClass*) is always true (for 99% of today's compilers at least). The type of the pointer however defines the size of the element it points to. And that is required for the compiler so he knows how much data he needs to get, or, when used with + or -, how much to add or subtract to get the address of the next or previous elements.
void * is basically a catch-all type. Any pointer type can be implicitly cast to void * without getting any errors. As such, it is mostly used in low level data manipulations, where all that matters is the data that some memory block contains, rather than what the data represents. On the flip side, when you have a void * pointer, it is impossible to determine directly which type it was originally. That's why you can't operate on the object it addresses.
if we try something like
typedef struct foo {
int key;
int value;
} t_foo;
void try_fill_with_zero(void *destination) {
destination->key = 0;
destination->value = 0;
}
int main() {
t_foo *foo_instance = malloc(sizeof(t_foo));
try_fill_with_zero(foo_instance, sizeof(t_foo));
}
we will get a compilation error because it is impossible to determine what type void *destination was, as soon as the address gets into try_fill_with_zero. That's an example of being unable to "use a void* to operate on the object it addresses"
Typically you will see something like this:
typedef struct foo {
int key;
int value;
} t_foo;
void init_with_zero(void *destination, size_t bytes) {
unsigned char *to_fill = (unsigned char *)destination;
for (int i = 0; i < bytes; i++) {
to_fill[i] = 0;
}
}
int main() {
t_foo *foo_instance = malloc(sizeof(t_foo));
int test_int;
init_with_zero(foo_instance, sizeof(t_foo));
init_with_zero(&test_int, sizeof(int));
}
Here we can operate on the memory that we pass to init_with_zero represented as bytes.
You can think of void * as representing missing knowledge about the associated type of the data at this address. You may still cast it to something else and then dereference it, if you know what is behind it. Example:
int n = 5;
void * p = (void *) &n;
At this point, p we have lost the type information for p and thus, the compiler does not know what to do with it. But if you know this p is an address to an integer, then you can use that information:
int * q = (int *) p;
int m = *q;
And m will be equal to n.
void is not a type like any other. There is no object of type void. Hence, there exists no way of operating on such pointers.
This is one of my favourite kind of questions because at first I was also so confused about void pointers.
Like the rest of the Answers above void * refers to a generic type of data.
Being a void pointer you must understand that it only holds the address of some kind of data or object.
No other information about the object itself, at first you are asking yourself why do you even need this if it's only able to hold an address. That's because you can still cast your pointer to a more specific kind of data, and that's the real power.
Making generic functions that works with all kind of data.
And to be more clear let's say you want to implement generic sorting algorithm.
The sorting algorithm has basically 2 steps:
The algorithm itself.
The comparation between the objects.
Here we will also talk about pointer functions.
Let's take for example qsort built in function
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
We see that it takes the next parameters:
base − This is the pointer to the first element of the array to be sorted.
nitems − This is the number of elements in the array pointed by base.
size − This is the size in bytes of each element in the array.
compar − This is the function that compares two elements.
And based on the article that I referenced above we can do something like this:
int values[] = { 88, 56, 100, 2, 25 };
int cmpfunc (const void * a, const void * b) {
return ( *(int*)a - *(int*)b );
}
int main () {
int n;
printf("Before sorting the list is: \n");
for( n = 0 ; n < 5; n++ ) {
printf("%d ", values[n]);
}
qsort(values, 5, sizeof(int), cmpfunc);
printf("\nAfter sorting the list is: \n");
for( n = 0 ; n < 5; n++ ) {
printf("%d ", values[n]);
}
return(0);
}
Where you can define your own custom compare function that can match any kind of data, there can be even a more complex data structure like a class instance of some kind of object you just define. Let's say a Person class, that has a field age and you want to sort all Persons by age.
And that's one example where you can use void * , you can abstract this and create other use cases based on this example.
It is true that is a C example, but I think, being something that appeared in C can make more sense of the real usage of void *. If you can understand what you can do with void * you are good to go.
For C++ you can also check templates, templates can let you achieve a generic type for your functions / objects.

Passing a pointer to an int array to a member function, error: invalid types 'int[int]' for array subscript

Ok, I'm fairly new to programming, and c++ so please take it easy on me. I am trying to write a program that takes in the dimensions of a metal plate for a 2-D finite element method analysis (thickness neglected). So, I created a class for my part (the plate), the elements for the mesh, and the nodes for the elements. The mesh will consist of square elements and will be applied over the front face of the plate. Right now, I'm working on getting the mesh sorted out before I move on to the element and node classes.
I'm using (or wanting to use) dynamic allocation to create a 2-D array (my mesh) containing the elements of the mesh. I'm trying to write a function, "meshingPart", to create the 2-D array with the number of rows being the height of the plate, and the columns being the length of the plate.
When I run the program, I get these errors and I'm not sure how to fix them:
In member function 'void PartClass::meshingPart(int&, int, int)':
error: invalid types 'int[int]' for array subscript
At global scope:
error: expected constructor, destructor, or type conversion before '(' token
Also, when I use my printPart() function, will it print the pointer's address, or the values of the array? I'm not completely sure about this, I'm also new to pointers.
Any help would be much appreciated! Thanks in advance.
class PartClass
{
private:
const int HEIGHT; // mm
const int LENGTH; // mm
const int WIDTH; // mm
const int SEED; // mm
const int MESHROW;
const int MESHCOL;
int *partMesh; // Mesh array - an int pointer
// Creates the mesh for the part by generating elements to fill the width and length
// of the part. The elements are stored in a 2-D array.
void meshingPart(const int &partMesh, int inRow, int inCol);
public:
// Constructs a part with the given parameters, seeds the part for the mesh,
// then creates the mesh by generating square elements with length = height = SEED.
PartClass(int inHeight, int inLength, int inWidth, int inSeed);
void printPart()
{
cout << "Part mesh:" << *partMesh << endl;
}
};
class ElementClass
{
private:
int elemID;
static int numElems;
// Shape functions:
int N1;
int N2;
int N3;
int N4;
public:
// Default constructor
ElementClass()
{
elemID = numElems;
numElems++;
};
};
PartClass :: PartClass(inHeight, inLength, inWidth, inSeed)
{
HEIGHT = inHeight;
LENGTH = inLength;
WIDTH = inWidth;
SEED = inSeed;
MESHROW = HEIGHT/SEED;
MESHCOL = LENGTH/SEED;
// Dynamically declares an array, gets memory, assigns address to partMesh.
partMesh = new int[MESHROW][MESHCOL];
meshingPart(&partMesh, MESHROW, MESHCOL);
}
void PartClass :: meshingPart(int &partMesh, int inRow, int inCol)
{
for( int i; i < inRow; i++)
{
for( int j; j < inCol; j++)
{
partMesh[i][j] = ElementClass();
}
}
}
There are multiple problems with the shown code, not a single problem. All of the problems must be fixed in order to resolve all compilation errors.
void PartClass :: meshingPart(int &partMesh, int inRow, int inCol)
The first parameter to this class method is declared as a reference to a single, lonely int. It is not an array, hence the code in this class method that treats it as an array will make your C++ compiler very, very sad.
int *partMesh; //
partMesh = new int[MESHROW][MESHCOL];
partMesh is declared as a pointer to an int. The new expression produces, effectively, a pointer to an array of MESHCOL ints. In C++ you cannot convert a pointer to an array into a different kind of a pointer.
Furthermore, nothing shown here requires the use of new in the first place. partMesh could simply be a std::vector<vector<int>>, and the new replaced by a strategic resize(). As an extra bonus your C++ program will automatically delete all this memory when it is no longer needed, not leak it, and also implement correct copy/move semantics where needed!
meshingPart(&partMesh, MESHROW, MESHCOL);
As we've concluded, the first parameter to the function is a reference to an array. Passing to it an address of a pointer to int will also not work.
Furthermore, since partMesh is a member of the same class, having one function in the class pass, in some form, a member of the same class to another class method accomplishes absolutely useful, whatsoever. Since it's a member of the same class it doesn't need passing, the class method can access it directly. This is what, after all, classes are all about.
In conclusion:
There are several problems regarding the C++ type system that are causing these compilation errors.
It is not necessary to even use new here, to initialize the pointer, and either its type needs to be adjusted to reflect that it's a pointer to a 2D array, or the new statement itself needs to be adjusted to allocate a one-dimensional array, since that's the only thing C++ allows you to convert to a plain pointer. And even that is overnegineered, since a std::vector will take care of all these pesky details by itself.
It is not necessary to even pass the member of the same class to the same class's method, as a parameter, just have the class method access it directly.
It's apparent that the likely process that produced the shown code was to write it in its entirety, and only try to compile after the whole thing was written. An avalanche of compilation errors is almost guaranteed any time this approach is used. It is far more productive to write a large program by writing only a few lines at a time, testing them, make sure they work correctly, and only then write a few more. This way, the number of errors that need to be fixed will remain quite small, and manageable.

Storing elements with different types into an array

How can I store elements of different types in an array?
The example below can lead to problems, because "wheels" can use 32-bit in an 8-bit array, so it fills the first 4 fields (if I understood it right). Can I use a shift operator << to overcome this problem, or is there an even better way to do it?
class Car
{
public:
Car();
~Car();
private:
int32 wheels; // 32 bit Integer
bool canDrive; // 1 bit Boolean
int16 doors; // 16 bit Integer
}
Car()
{
wheels = 4;
canDrive = true;
doors = 2;
}
int main()
{
Car testCar;
int08 tmpArray[ARRAY_SIZE] = { 0 }; //8-bit Integer array
tmpArray[0] = testCar.wheels; //Store 32-bit Integer into 8-bit Integer
tmpArray[1] = testCar.canDrive; //Store 1-bit Boolean into 8-bit Integer
tmpArray[2] = testCar.doors; //Store 16-bit Integer into 8-bit Integer
/* Do something with tmpArray */
return 0;
}
I don't understand your code - do you simply want an array of Car?
How can I store elements of different types in an array?
Use std::variant or boost::variant. As an example, assuming that you want an array of either int or float:
using i_or_f = std::variant<int, float>;
std::array<i_or_f, 5> arr;
arr[0] = i_or_f{5};
arr[1] = i_or_f{0.4f};
1.) Your testCar is not constructed properly. No data has been assigned to wheelsm, canDrive, ... . Thus, setting other values with this undefined data will lead to undefined behavior.
2.) Your member values of the class car are private (This is a nice thing, since it ensures your data encapsulation). You will not be able to access them as you like.
3.) You can do a typecast form your different type. But why would you do so. You are loosing the type safety.
4.) Can you please explain, what is the purpose in storing your class members in an array. If you do not like to worry about the actual type consider using auto.
This is also worth reading.
How can I store elements of different types in an array?
Use polymorphism.
Create an std::vector of base class pointers.
std::vector<Car_t*> carVec;
Create pointers to derived instances and install in vector. Each derived instance is a different type.
carVec.push_back(new Derived_A_FromCar);
carVec.push_back(new Derived_B_FromCar);
carVec.push_back(new Ferrai_t);
carVec.push_back(new Fairlane_t);
... etc

Operating on an array which is passed by reference to it's first element

So I have this piece of code in separate Chromosome.cpp which is supposed to turn any double into binary with given precision and store it in an array:
char* Chromosome::convert()
{
double precision = CHROMOSOME_PRECISION;
int toconvert = (int)(value * (1.0 / precision));
char binary[CHROMOSOME_LENGTH] = { (toconvert < 0)?'1':'0' };
_itoa_s(toconvert, binary, 2);
printf("Value is: %i, binary representation is: %s\n", toconvert, binary);
return &binary[0];
}
And then I want to operate on an created array, like for example swap halves of it (for example from 11110000 and 11001100 make 11111100 and 1100000). So I made another .cpp called Operations, where I will hold functions that will modify my chromosomes. But I have no idea what I should write inside function like that:
void cross(Chromosome a, Chromosome b)
{
}
I've tried things like char* aBinary[32] and then aBinary[0] = a.convert(); but it's not working...
return &binary[0];
You're returning a pointer to a local array which is being destroyed. Undefined behavior when the caller tries to use it.
You can either have the caller provide a buffer which convert() fills in, or have convert() perform dynamic allocation and require the caller to deallocate.
If using dynamic allocation, prefer an object that automatically manages the memory, such as std::string, std::vector<char>, or std::unique_ptr<char[]> rather than a raw pointer.

How to have a C++ stack with more than one data type?

Here's the problem:
I am currently trying to create a simple stack-based programming language (Reverse Polish Notation, FORTH style) as a component of a larger project. I have hit a snag, though.
There is no problem with creating a stack in C++ (by using std::vector<>) that would contain one type of element (I could use the syntax std::vector<double> Stack, for instance).
However, a programming language needs to be able to hold multiple data types, such as ints, doubles, strings, and 3D vectors (as in physics vectors with X, Y, and Z components), just to name some simple things.
So, is there a construct in C++ that I could use as a stack that would be able to store more than one kind of primitive type/object/struct?
Sure, one way is to use a tagged union:
enum Type { INTEGER, DOUBLE, /* ... */ };
union Data {
uint64_t as_integer;
double as_double;
// ...
};
struct Value {
Type type;
Data data;
};
The storage for as_integer, as_double, etc. will be overlapped, so a Value structure will take up two words of storage, and your stack will have type std::vector<Value>. Then you access members of data according to the value of type:
void sub(std::vector<Value>& stack) {
// In reality you would probably factor this pattern into a function.
auto b = stack.back();
stack.pop_back();
assert(b.type == INTEGER);
auto a = stack.back();
stack.pop_back();
assert(a.type == INTEGER);
Value result;
result.type = INTEGER;
result.data.as_integer = a.data.as_integer - b.data.as_integer;
stack.push_back(result);
}
Of course, Forths are usually untyped, meaning that the stack consists of words only (std::vector<uint64_t>) and the interpretation of a data value is up to the word operating on it. In that case, you would pun via a union or reinterpret_cast to the appropriate type in the implementation of each word:
void subDouble(std::vector<Data>& stack) {
// Note that this has no type safety guarantees anymore.
double b = stack.back().as_double;
stack.pop_back();
double a = stack.back().as_double;
stack.pop_back();
Data result;
result.as_double = a - b;
stack.push_back(result);
}
void subDouble(std::vector<uint64_t>& stack) {
double b = reinterpret_cast<double&>(stack.back());
stack.pop_back();
double a = reinterpret_cast<double&>(stack.back());
stack.pop_back();
double result = a - b;
stack.push_back(reinterpret_cast<uint64_t&>(result));
}
Alternatively, you can store not values but pointers to instances of a class Value from which other value types such as Integer or Double would derive:
struct Value {};
struct Integer : Value { uint64_t value; };
struct Double : Value { double value; };
// ...
Your stack would have type std::vector<unique_ptr<Value>> or std::vector<Value*>. Then you needn’t worry about different value sizes, at the cost of making wrapper structures and allocating instances of them at runtime.
I would suggest to use the inheritance. Make common base class for the objects you need to store, and make a vector of base types. The store all the inheriting objects in this vector.
Since c++ is an object-orientated language, you might just use inheritance. Here is a quick example taken from http://www.cplusplus.com/forum/general/17754/ and extended:
#include <iostream>
#include <vector>
using namespace std;
// abstract base class
class Animal
{
public:
// pure virtual method
virtual void speak() = 0;
// virtual destructor
virtual ~Animal() {}
};
// derived class 1
class Dog : public Animal
{
public:
// polymorphic implementation of speak
virtual void speak() { cout << "Ruff!"; }
};
// derived class 2
class Cat : public Animal
{
public:
// polymorphic implementation of speak
virtual void speak() { cout << "Meow!"; }
};
int main( int argc, char* args[] )
// container of base class pointers
vector<Animal*> barn;
// dynamically allocate an Animal instance and add it to the container
barn.push_back( new Dog() );
barn.push_back( new Cat() );
// invoke the speak method of the first Animal in the container
barn.front()->speak();
// invoke all speak methods and free the allocated memory
for( vector<Animal*>::iterator i = barn.begin(); i != barn.end(); ++i )
{
i->speak();
delete *i;
}
// empty the container
barn.clear();
return 0;
}
The solution for storing different types is a tagged union
enum Type { INT, STRING, DOUBLE, POINT2D, VECTOR, OBJECT... };
union Data {
int int_val;
double double_val;
struct point2D { int x, int y };
struct { int v3, int v2, int v1, int v0 }; // you can even use unnamed structs
// ...
};
struct StackElem {
Type type;
Data data;
};
In C++ it's even better to use std::variant (or boost::variant in older C++ standards), which might use a tagged union under the hood
However there's no need to use a single stack for all when using the reverse Polish notation. You can use a value stack and a separate operator stack. For every operator on the operator stack you pop the corresponding number of parameters from the value stack. That'll make things easier and save memory since you can use a small char array for operators (unless you need more than 255 operators), and no memory wasted for saving the type as well as the bigger-than-needed data field in the struct like above. That means you don't need a type OPERATOR in the Type enum
You can use a double type stack for all numeric types because a double can contain all int type's range without loss of precision. That's what implemented in Javascript and Lua. If the operator needs more than 1 parameter then just push/pop all of them just like what a compiler does when evaluating a function. You don't need to worry about int operations anymore, just do everything in double, unless there are specific int operators. But you may need different operators for different types, for example + for double addition, p or something like that for vector addition. However if you need 64-bit int then a separate integer type is needed
For example if you need to add 2 3D vectors, push 3 dimensions of the first vector, then the other. When you pop out a vector operator from the operator stack, pop 3 dimensions of the 2 vectors from value stack. After doing the math on it, push resulting 3 dimensions to stack. No need for a vector type.
If you don't want to store int as double then you can use NaN-boxing (or nunboxing/punboxing) like Firefox's JS engine, in which if the value is int then the upper 16 of 64 bits are 1s, otherwise it's double (or pointer, which you probable wouldn't use). Another way is type tag in 3 lower bits in old FFJS engines. In this case it's a little bit complicated but you can use the same operator for every type. For more information about this read Using the extra 16 bits in 64-bit pointers
You can even use a byte array for storing all data types and read the correct number of bytes specified by the operator. For example if the operator indicated that the next operand must be an int, just read 4 bytes. If it's a string, read the 4 bytes of string length first then the string content from the stack. If it's a 2D point of int read 4 bytes of x and 4 bytes of y. If it's a double read 8 bytes, etc. This is the most space efficient way, but obviously it must be traded by speed