[SOLVED] Dynamically adding/removing widgets from an already shown window
-
wrote on 24 Oct 2014, 12:55 last edited by
Hi,
I've got a little configuration window (QDialog) which contains (at beginning) 10 QTextEdits.
The user should have the option to add/remove QTextEdits from this QDialog window by clicking "Add"/"Remove" button.
My code looks like that:
@ window_settings::window_settings(QWidget* parent) : QDialog(parent)
{
this->button_add = new QPushButton("add", this);
this->button_remove = new QPushButton("remove", this);
connect(this->button_add, SIGNAL(clicked()), this, SLOT(addEdit()));
connect(this->button_remove, SIGNAL(clicked()), this, SLOT(removeEdit()));this->list_edits = new QList<QTextEdit*>; for(int i = 0; i < 10; i++) { this->list_edits->append(new QTextEdit(this)); this->list_edits->last()->setGeometry(QRect(5, (5+i*30), 150, 26)); }
}
void window_settings::addEdit()
{
this->list_edits->append(new QTextEdit(this));
this->list_edits->last()->setGeometry(QRect(5, (5+(this->list_edits->count()-1)*30), 150, 26));
this->list_edits->last()->show(); // --> why do I have to show the new Widget separatly??
this->adjustSize();
}void window_settings::removeEdit()
{
if(this->list_edits->count() > 1)
{
QTextEdit *edit_last = this->list_edits->last();
this->list_edits->removeLast();
delete edit_last;
}
else
QMessageBox::information(this, "Warning", "Minimum edit count is 1...", QMessageBox::Ok);
}@
Everything works as I like it, but I have a few questions about it:-
Is that the right way to dynamically add/remove widgets from an already existing window? (I don't want to use layouts in this case, so I'd like to know whether this is the right way to do it without layouts?)...
-
Why does it even work? Why don't I even have to update or close/show the window again before the changes were applied? I'm confused how QDialog automatically updates itsself...
-
Why do I have to show each dynamically added Widget separatly (edit->show())? I thought, I only have to specifically show that widget, that will be the window itsself??
-
Is there an easier way to remove items from a QList and automatically delete these items?
I hope somebody can help me with that, thank's.
-
-
- Yup, that's ok. Just a question - why don't you wan't to use layouts? You would avoid all those hacky position calculations. You assume things like border sizes etc but they can vary depending on theming settings of the user. Also if a user has a DPI setting adjusted in his OS the hardcoded height might cause the text to be clipped.
2/3. There are two possible behaviors here depending on whether or not you showed the dialog before adding children.
- If the dialog is not yet visible: you don't need the calls to show() or adjustSize(). The geometry and position of children has not yet been evaluated because it was not needed. When you finally show the dialog (via show() or exec()) the geometries of children are evaluated and the size of the dialog is adjusted to accommodate the children. All this happens in the show event handler of the dialog.
- If you already showed the dialog: the show event handler has already fired. Adding a child to a visible widget doesn't affect its display or geometry so you need to show it manually. Because no layout manager is present you also need to adjust its size manually.
That being said you have all this extra work and opportunities for a mistake because you're not using layouts.
- list_edits->takeLast()->deleteLater();
Btw. Why is list_edits a dynamically allocated member? Why not just a simple non-pointer member? That's a performance and maintainability hit.
-
wrote on 26 Oct 2014, 08:45 last edited by
Hi Chris,
first of all, thank's a lot for that detailed answer! You helped me much with that.
[quote] why don’t you wan’t to use layouts[/quote]Normally, I do use them, but for the moment I just wanted to know how to basically manage the dynamically adding/removing of widgets (just for understanding) :)
You're definitly right, layouts make everything easier and I always use them, but I wanted to avoid an offered solution with layouts..[quote]2/3. There are two possible behaviors here depending on whether or not you showed the dialog before adding children[/quote]What I'm asking myself then is, when I already showed the Window and I have to show every dynamically new added widget itsself to be applied, why don't I have to update window after deleting a widget? When I delete a widget that is already visible, window "notices" that immediately and widget removes... so there has to be a permanently loop where window checks which widgets are still "alive" ?
[quote]4. list_edits->takeLast()->deleteLater();[/quote]Great! Just a little change:
@delete list_edits->takeLast();@
Otherwise, "adjustSize()" has no effect because last edit will be deleted later :-P[quote]Btw. Why is list_edits a dynamically allocated member? Why not just a simple non-pointer member? That’s a performance and maintainability hit.[/quote]There we have the discussion again: When do I need stack and when do I prefer heap instead... I still don't know it so I decided to do it all dynamically... In some cases, I have no other idea. Example:
@QSettings* settings = new QSettings("/main.ini", QSettings::IniFormat, this); // works fine
QSettings settings = QSettings("/main.ini", QSettings::IniFormat, this); // compiler error, but I don't know why??@
What do you say to that? -
bq. so there has to be a permanently loop where window checks which widgets are still “alive” ?
No, that would be a waste of cpu cycles. When a QObject is destroyed it notifies its parent triggering its childEvent() handler with parameter QEvent::ChildRemoved. A widget reacts to this by refreshing itself to prevent the user from interacting with non-existing widget.
As to dynamic allocations. A good rule of thumb is - if you're gonna use a "thing" across the whole lifetime of the class make it a non-pointer member. In your case you can call add or remove anytime so the list is a "long living thing". Exception being of course if it's large (like stack overflow large), but not here. Remember that a list is usually implemented via just a couple of pointers to another dynamically allocated space. Allocating the few pointer bytes dynamically is an unnecessary work and another level of indirection for the cpu.
As for things like QSettings - if it's a "thing" inside a function that is used only locally for a brief period prefer stack. It's faster and the "thing" is removed automatically for you on scope exit.
One thing to remember when creating stack objects - don't give them parent. A parent deletes all of its children on destruction so it's gonna try to call delete on an object that went out of scope and was already deleted automatically.
@
//OK, passing "this" as parent. delete will be called on the
//settings object when "this" is destroyed
QSettings* settings = new QSettings("/main.ini", QSettings::IniFormat, this);//Two things wrong here
//You are using a copy constructor. QObjects are by design non-copyable.
//Also you are passing "this" as parent. The object will be destroyed when it goes
//out of scope and then "this" will try to delete it again, that's undefined behavior
QSettings settings = QSettings("/main.ini", QSettings::IniFormat, this);//The preferred way in this case
//Object will go out of scope (on next } ) and will be destroyed
QSettings settings("/main.ini", QSettings::IniFormat);
@ -
wrote on 26 Oct 2014, 22:42 last edited by
[quote] When a QObject is destroyed it notifies its parent triggering its childEvent() handler with parameter QEvent::ChildRemoved. A widget reacts to this by refreshing itself to prevent the user from interacting with non-existing widget.[/quote]Ah that's the way it goes.. So the deletable QObject manages this by informing it's parent via it's destructor I think, because that's the last function which is called before the QObject is finally deleted, right?
[quote]As to dynamic allocations. A good rule of thumb is – if you’re gonna use a “thing” across the whole lifetime of the class make it a non-pointer member[/quote]Ok, I'll heed that advice, but I always thought it's exactly the other way round: dynamically allocated objects have longer lifetime, because they are not bound to the scope. When I do it like you adviced, then I won't need any pointers, because all of my class members exist till object is destroyed and also my object members do not reserve that much memory, so stack will be the right way?
[quote]One thing to remember when creating stack objects – don’t give them parent. A parent deletes all of its children on destruction so it’s gonna try to call delete on an object that went out of scope and was already deleted automatically.[/quote]Oh, yes that sounds logical! Thank's.
And there is my mistake, unfortunatelly I tried to create the QSettings object with the copy constructor, now I also understand the compiler error message... -
bq. So the deletable QObject manages this by informing it’s parent via it’s destructor I think, because that’s the last function which is called before the QObject is finally deleted, right?
Right.
bq. so stack will be the right way?
Don't confuse "stack" with "non-pointer member". Consider this:
@class Foo { Bar bar; };
...
Foo* foo = new Foo();@
bar is a non-pointer member, yet it's allocated on the heap with the rest of the Foo object.This example also shows what the "rule" was about. Foo is allocated on a heap, and bar is part of that same allocation block so access is fast. If foo was a pointer it would itself still be a fragment of the Foo allocation block, but the thing it points to would be in totally different part of memory. More "hops" to get to the thing - less performance and less cache friendly.
Ok, a single pointer indirection is not that big deal usually but if we can do it better and at the same time type less code then why not.On the other hand there's this:
@
class Foo { Bar bar; };
...
Foo foo;
@
If Bar is big (eg. larger than a stack) this will crash your app.So if you don't know how your class will be created (stack or heap), and the member is big, allocate it dynamically(heap). The Foo object will be allocated on the stack but that's ok cause it's small. The big thing pointed by bar is on the heap.
In your case you're in scenario 1. QList itself is small so it can be safely allocated together with the object containing it either on the stack or heap. QList allocates its contents on the heap so we don't care if they're big.
-
wrote on 27 Oct 2014, 09:35 last edited by
Hmm, I think that is the basic point that I need to understand to have the "Aha!" effect, but I don't get it so far.
Szenario 1:
@class Foo { Bar bar; };
...
Foo* foo = new Foo()@
[quote] If foo was a pointer it would itself still be a fragment of the Foo allocation block, but the thing it points to would be in totally different part of memory.[/quote]But foo is a pointer, isn't it?? I don't understand what you mean with "different part of memory"... I thought that each member of a class object will be stored into the same memory block, either on stack:
@class Foo { Bar bar; };
Foo foo; // bar object is now on stack@
or on heap:
@class Foo { Bar bar; };
Foo *foo = new Foo(); // bar object is now on heap@
When I've got szenario 1:
@Foo *foo = new Foo();@
Why is the pointer *foo itsself part of the Foo object memory block? And why should it point to a totally different memory block? I thought it exactly points to the memory block were it's Foo object is stored?Second thing that confuses me is szenario 2:
@ class Foo { Bar bar; };
...
Foo foo;
@
[quote]So if you don’t know how your class will be created (stack or heap), and the member is big, allocate it dynamically(heap). The Foo object will be allocated on the stack but that’s ok cause it’s small. The big thing pointed by bar is on the heap.[/quote]What do you mean with that? Related to your code example, nothing is allocated on heap, right? So do you mean, that in case of big members of an object, I should create the object on stack, but it's members on heap? Like:
@class Foo {Bar *bar};
Foo foo;
Foo::Foo()
{
this->bar = new Bar();
}@
Is that what you mean? -
Lets assume that a size of an empty class is 4 bytes. This actually varies across compilers , platforms and bitness but lets simplify for the sake of example. I'll also assume 32bit system which means size of any pointer is 4. I'm not gonna consider memory alignment etc.
Ok, so lets break this down to these variants:
-
- Bar is "big", Foo is allocated on the stack:
@
class Bar { char stuff[10000000000]; };
class Foo( Bar bar };
...
Foo foo;
@
stack: 4(Foo) + 4(Bar) + 10000000000(stuff) = 10000000008
heap: 0
indirections: 0
This will result in a stack overflow and a crash.
- Bar is "big", Foo is allocated on the stack:
-
- Bar is "small" and allocates stuff dynamically. Foo is allocated on the stack:
@
class Bar { char* stuff; Bar(){ stuff = new char[10000000000]; } };
class Foo( Bar bar };
...
Foo foo;
@
stack: 4(Foo) + 4(Bar) + 4(stuff) = 12
heap: 10000000000(thing that stuff points to)
indirections: 1(stuff pointer)
That's an OK scenario. Heap is used for large member.
- Bar is "small" and allocates stuff dynamically. Foo is allocated on the stack:
-
- Bar is "big", Foo is allocated on the heap:
@class Bar { char stuff[10000000000]; };
class Foo( Bar bar };
...
Foo* foo = new Foo();@
stack: 4(foo pointer)
heap: 4(Foo, what foo points to) + 4(Bar) + 10000000000(stuff) = 10000000008
indirections: 1(foo pointer)
Ok if you can guarantee that Foo will always be allocated dynamically. In practice that's hard to do and rarely used (maybe in some factory pattern scenarios).
- Bar is "big", Foo is allocated on the heap:
-
- Bar is "small" and allocates stuff dynamically, Foo is allocated on the heap:
@
class Bar { char* stuff; Bar(){ stuff = new char[10000000000]; } };
class Foo( Bar bar };
...
Foo* foo = new Foo();
@
stack: 4(foo pointer)
heap: 4(Foo) + 4(Bar) + 4(stuff) + 10000000000(what stuff points to) = 10000000012
indirections: 2(foo pointer and stuff pointer)
This is actually your scenario. Your class(like Foo) is allocated on the heap and it has a QList member(like Bar) that also allocates stuff on the heap.
- Bar is "small" and allocates stuff dynamically, Foo is allocated on the heap:
These are the basic variants you should usually consider (well maybe not 1) ).
In contrast, when you originally allocated QList dynamically it's like this:
-
- Bar is "small, allocates dynamically and is itself allocated dynamically, Foo is on the heap:
@
class Bar { char* stuff; Bar(){ stuff = new char[10000000000]; } };
class Foo( Bar* bar; Foo(){ bar = new Bar(); } };
...
Foo* foo = new Foo();
@
stack: 4(foo pointer)
heap: 4(Foo) + 4(bar pointer) + 4(Bar) + 4(stuff) + 10000000000(what stuff point to) = 10000000016
indirections: 3(foo pointer, bar pointer and stuff pointer)
As you can see this is the longest to type, takes most memory and has most indirections.
- Bar is "small, allocates dynamically and is itself allocated dynamically, Foo is on the heap:
Now what I mean by "indirection". When you allocate dynamically you are given a memory chunk from the heap. You can't assume anything as to actual address. For example two consecutive allocations can be created in far apart memory regions. CPUs got caches and the further apart two memory regions lie the less likely they will live in the same cache line. So the more pointers you need to "hop" to get to the actual thing the more cache misses you will get and the worse performance.
-
-
One more small thing:
bq. I thought that each member of a class object will be stored into the same memory block, either on stack (...) or on heap
Yes, but if that object is allocated dynamically and the member is allocated dynamically you have a situation like this:
@
class Bar{ char* stuff; /like before/ };
class Foo{ Bar* bar; /like before/ };Foo* foo = new Foo();
address of foo: 1 //some address on the stack
address of *foo: 1000 //some random address on the heap
address of bar: 1004 //address of *foo + offset into the class
address of *bar: 5000 //some other random address on the heap
address of stuff: 5004 //address of *bar + offset into the class
address of actual data *stuff: 7000 //yet another random address
@In contrast in the 3) scenario you get this:
@
class Bar{ char stuff[1000000] };
class Foo{ Bar bar; };Foo* foo = new Foo();
address of foo: 1 //some address on the stack
address of *foo: 1000 //some random address on the heap
address of bar: 1004 //address of *foo + offset into the class
address of stuff: 1008 //address of bar + offset into the class
@
As you can see everything is close to each other so more cache friendly. -
wrote on 27 Oct 2014, 15:25 last edited by
Ahh so great! :D I got it! Thank you so much, you have a gift for explaining things in a way that even the greatest newbie understands it. At the same time I'm trying to understand memory management by reading a book but it's really hard to get used to it without someone who explains things apposite to my questions..
Well, then there are two final questions left:
- [quote]
3) Bar is “big”, Foo is allocated on the heap:
class Bar { char stuff[10000000000]; };
class Foo( Bar bar };
...
Foo* foo = new Foo();
stack: 4(foo pointer) heap: 4(Foo, what foo points to) + 4(Bar) + 10000000000(stuff) = 10000000008 indirections: 1(foo pointer)
Ok if you can guarantee that Foo will always be allocated dynamically. In practice that’s hard to do and rarely used (maybe in some factory pattern scenarios).[/quote]Let's take a look at a class Foo with lot's of big members:
@class Foo
{
char stuff_1[1000000000], stuff_2[1000000000], stuff_3[1000000000],..., stuff_n[1000000000];
};@
In this case, it would be logical for me to allocate every member as non_pointer and only the whole Foo object dynamically on heap, so I only have 1 indirection (otherwise it could be 'n' indirections, maybe a hundred), and a lot of memory in one chunk is stored in heap.
But you say, this way is only rarely used and not the common way. Would you prefer your 1th method here and create every member dynamically with maybe 100 or 500 indirections?- When I now take a look at one of your posts:
[quote]As to dynamic allocations. A good rule of thumb is – if you’re gonna use a “thing” across the whole lifetime of the class make it a non-pointer member.[/quote]How can I substantiate that with the recently gained knowlege? I now know that allocating things on heap is only usefull for "big" objects, but you're talking from "lifetime", what doesn't have anything to do with size. So why is it better to allocate long living objects on stack?
- [quote]
-
-
Well it depends ;) You can of course do that, but it's hard to guarantee that someone someday won't write "Foo foo;" and crash the whole thing. It's a maintainability burden but sometimes has its uses. It comes down to a design decision.
-
Lifetime and size are two different considerations orthogonal to each other. Imagine one being an X axis and another Y axis. As a programmer its your task to try and hit that sweet spot on the diagonal ;) You just weight the current needs and make a decision.
Apart from all these performance considerations I'll give you an example from another perspective:
@
class Foo {
Bar bar;
void doStuff1() { /* do something with bar*/ }
void doStuff2() { /* do something with bar*/ }
void doStuff3() { /* do something with bar*/ }
};
@
@
class Foo {
Bar* bar = nullptr;
void initializeBar() { try { bar = new Bar(); } catch(std::bad_alloc) { /huh... fix me/ } }
Foo() { initializeBar(); }
~Foo( delete bar; );
void doStuff1() { if(!bar) initializeBar(); /* do something with bar/ }
void doStuff2() { if(!bar) initializeBar(); /* do something with bar/ }
void doStuff3() { if(!bar) initializeBar(); /* do something with bar/ }
};
@
Which code do you consider easier to read, to write without a bug, to change, to work with and which do you think runs faster? Do you think the second class is correctly implemented i.e. every case is covered? Is it newbie friendly? -
-
wrote on 28 Oct 2014, 07:53 last edited by
[quote]Lifetime and size are two different considerations orthogonal to each other.[/quote]Yes, I understand that. What I do not yet understand is why long living objects should be (except they are very big) allocated on a stack. What's the reason for that? I mean, stack has the problem that every created object only lives as long as scope. Well, you said "if a member should live the whole livetime of it's class object", ok, but where is the difference to a dynamically created member? Why shouldn't this member also be able to live the whole lifetime of the class?
The only idea I have is, that a good programmer maybe tries to delete dynamically allocated things quickly, because this always means an indirection and that pushes performance down... am I on the right way?[quote]Which code do you consider easier to read, to write without a bug, to change, to work with and which do you think runs faster? Do you think the second class is correctly implemented i.e. every case is covered? Is it newbie friendly?[/quote]Well, I think the first example is much better to read, to work with etc., but I don't understand two things:
- Why do I have to check the existence of 'bar' in every member function when I initialize it in constructor? I mean, I can't call these member functions without an object as long as I don't declare them as static, right?
- When I take a look at my previous post, I asked you which option would be the best in case of 'n' big members in a class, and you said that it depends, but to avoid stack crashes it's maybe better to create each member dynamically instead. So in this case, I have to do it like you described it in example two, right? Example one is only possible as long as I create the whole object dynamically on heap (in case of lot's of big members 'bar')...
EDIT: A third (fourth) question:
In every Qt example/tutorial, everything (QPushButtons, QTextEdits, every widget) is created dynamically. Is there a reason for that? Example:
@ QWidget *window = new QWidget;
QPushButton *button1 = new QPushButton("One");
QPushButton *button2 = new QPushButton("Two");
QPushButton *button3 = new QPushButton("Three");
QPushButton *button4 = new QPushButton("Four");
QPushButton *button5 = new QPushButton("Five");
QGridLayout *layout = new QGridLayout;@
Maybe they also prefer your second variant and create every member dynamically... but that always means a thousand indirections... -
bq. why long living objects should be (...) allocated on a stack
I don't believe I ever said that (at least Ctrl+F doesn't find that) and if I did then it was a mistake on my part. I do believe I said that long living objects should be non-pointer members, which could be either stack of heap, depending on how the class object is allocated.
bq. am I on the right way?
You don't have to "delete quickly". It depends on the application.
There's no one answer to rule them all here. Usually when you have a pointer member it's because some of these reasons:-
it's an ok state for the object if the pointer is null, eg. a QWidget can have a parent pointer or null.
-
you need a fine-grained control over when or where the object is allocated and destroyed eg. using custom allocator, placement new etc. Example: QListView can have a model shared with other instances and managed by external thing, so it keeps only a pointer to it
-
the state is shared i.e. you keep one copy of data and point to it from multiple instances. Additionally if these objects are alll to "own" the data then it's a good place for smart-pointers like shared_ptr or QSharedPointer
-
if some library you use enforces that eg. in case of QObjects. If you use a non-pointer QWidget member and put it in a layout then it's that double delete error.
There are probably more cases, but these are the few examples.
bq. Why do I have to check the existence of ‘bar’
Maybe you don't have to. It depends on how you handle that exception in the initializeBar - does it always succeed or can a null slip by. It's just an example. The point is that pointers are harder to keep track of and reason about. I've seen a lot of code in the past. Sometimes classes 3-4 pages long with thousands of lines of member code. I'm not saying it's a good thing but these are often realities of our jobs. When you need to modify such a class in a single spot and you encounter a pointer it really encourages to do "defensive programming" i.e. check all pointers everywhere. It's not a good style for many reasons but sometimes the only way if you don't want to spend a week on the problem.
bq. I asked you which option would be the best in case of ‘n’ big members in a class
I can only repeat myself here - it depends. The code was somewhat artificial to show how this works. I can't think of a real case where I would create such large structures. But for the sake of example: yes, you could go with option 2) or you could do something like that:
@
class Foo {
private:
Bar bar; //bar is big
Foo(); //notice private constructor, prevents stack allocations by user
public:
static Foo* produce() { return new Foo(); } //safe, always on the heap
};Foo* foo = Foo::produce(); //the only way to get a Foo is from the heap
@
This would correspond to example 3)bq. In every Qt example/tutorial, everything (QPushButtons, QTextEdits, every widget) is created dynamically. Is there a reason for that?
It's not "everything". Closer to the truth is "everything derived from QObject". Notice that for example Qt containers like lists don't fall into that category.
Qt is designed around parent-child relationships between objects. As i said earlier this is to prevent the double-delete trap, because parent calls delete on every child, and if it happens to be non-pointer member removed automatically then it's a problem. Yes, it's an indirection, but that's often a negligible cost. If you have to "chase the pointer" often, eg. in a tight loop jump from one place to another than it can be a performance problem. But in case of Qt objects you usually take the pointer and do a lot of stuff to it. The cost of that initial indirection drowns in the sea of further operations. For example something like this: textEdit->setText("foo"). The cost of setting the text, sending a refresh signal, drawing to the screen(!) and calling any connected slots is probably thousands more than that one little -> there. You should not overthink that. Indirection is not a bad thing. "Too much" indirection is.
-
-
wrote on 28 Oct 2014, 15:03 last edited by
[quote]I don’t believe I ever said that (at least Ctrl+F doesn’t find that) and if I did then it was a mistake on my part. I do believe I said that long living objects should be non-pointer members, which could be either stack of heap, depending on how the class object is allocated.[/quote]Totally right, excuse me for that! I'm more and more confusing everything now, I think I lay to much stress on that issue now. I'm also sorry about illegaly using the double quotes without exactly reciting, that shouldn't be a formality.
I understand that you have a completely other point of view to that issue, because you already had to manage very big code chunks where this functionallity gets more weight.
For me, being an absolute beginner in Qt, I'll take the information you gave me and try it on my own.
I think for my little code snippets, I'm on the right way to declare QObject based class members and very big members as pointer members dynamically, the other stuff as non-pointer members.Maybe on last question to get more overview about runtime performance:
In some topics people posted the speed of a code snippet (runtime), e.g. to test which loop is faster or anything else. They measured it and posted the outcome (like 0.02s). Does that require an additional program or how can I measure the speed of execution of a code chunk?
PS: I know, this has nothing to do with Qt, I'm sorry about that, but maybe the answer to that is simple and this topic can finally be marked as [solved] :-P -
For timing you can use "QElapsedTimer":http://qt-project.org/doc/qt-5/qelapsedtimer.html#details
Notice though that it only makes sense on a greater scale i.e. there's no point in trying to measure a single short instruction (like adding two floats) cause you'll get garbage results varying widely across multiple runs. In that case the usual way is to run that instruction many times (like thousands or millions), measure that and divide by the number of repetitions to get an average. That's because of the multitasking, multicore and I/O bound nature of today computers and OSes. You can't get a precise measure of single thing if it's below certain speed threshold.
This doesn't apply of course to long operations (i.e. in the range above few milliseconds). These you can quite consistently measure on a single case basis. -
wrote on 28 Oct 2014, 21:50 last edited by
Thank you. I'll take a look at it.
1/16