OO-modeling: c++too many choices

  • container of polymorphic Animals (having vtbl);
  • Nested containers; singletons;
  • class inheriting from multiple supertypes ..

In these and other OO-modeling decisions, there are many variations of “common practices” in c++ but in java/c# the best practice usually boils down to one or two choices.

No-choice is a Very Good Thing, as proven in practice. Fewer mistakes…

These dynamic languages rely on a single big hammer and make everything look like a nail….

This is another example of “too many variations” in c++.

## marketable syntax nlg: c++ > j/c#

Every language has poorly understood syntax rules, but only in c++ these became fashionable, and halos in job interviews !

  • ADL
  • CRTP
  • double pointers
  • hacks involving void pointers
  • operator overloading to make smart ptr look like original pointers
  • TMP hacks using typedef
  • TMP hacks using non-type template param
  • universal reference vs rvr
  • rval: naturally occurring vs moved
    • const ref to extend lifetime of a naturally occurring rval object

c++11 throwing dtor: a few technicalities for IV

  • STL containers and std::swap() lose all exception guarantees when a payload class dtor throws
  • In a normal context (without double exception), if a dtor throws, it’s same as new() throwing exception. Stack unwinds as expected, and all subsequent dtors run as expected.
  • in c++11, all dtors are implicitly noexcept. If such a dtor throws, it triggers std::terminate() by default

Q: what if your dtor encounters a critical error but not allowed to throw exception?
A: https://www.kolpackov.net/projects/c++/eh/dtor-1.xhtml mentions a pre_destroy() member function that can throw. Dtor would call this function but catch and ignore the exception. Client code can also call this same function but handle the exception intelligently.

CRTP,ADL,thread_local to replace old-school

  • — thread_local variable to replace member data .. q[static thread_local ] in %%production code
  • eliminates pollution
  • — ADL  is often chosen to replace member operator and methods.. ADL #namespace
  • reduces coupling
  • –CRTP is often chosen to replace runtime binding (dynamic dispatch) of virtual function call
  • Template only
  • shaves a few clock cycles in HFT

I think both are advanced.

pure virtual dtor

pure virtual dtor is a low-value obscure topic in 1) interview 2) zbs. So I won’t spend too much time on it.

https://stackoverflow.com/questions/1219607/why-do-we-need-a-pure-virtual-destructor-in-c addresses the rationale and justification for pure virtual dtor

https://www.geeksforgeeks.org/pure-virtual-destructor-c/ explains this dtor must be defined somewhere or compiler/linker would complain.

demo: static method declare/define separately n inherited

Low complexity in this demo, but if you lack a firm grip on the important details here, they will add to the complexity in a bigger code base.

  • When subclass is compiled, compiler complains about undefined sf() since it sees only the declaration. You need “g++ der.cpp base.cpp”.
  • Note the static method is inherited automatically, so you could call der::sf().
#include <iostream>
struct base{
  static void sf();
///////// end of header /////////
#include "base.h"
using namespace std;
void base::sf(){ // no "static" please
///////// end of base class /////////
#include "base.h"
using namespace std;
struct der: public base{};
int main(){
///////// end of sub class /////////

returning const std::string #test program#DeepakCM

1) returning a const std::string is meaningful.. it disallows f().clear(), f().append, f().assign() etc. Deepak’s 2019 MS interview.

2) returning a const int is useless IMO. [1] agrees.

3) I agree with an online post. Returning a const ptr is same as returning a non-const ptr. Caller would clone that const ptr just as it clones a mutable ptr.

I would say what’s returned is an address, just like returning an int value of 315.

int i=444;
int * const pi = &i;
int * p2 = pi;
int main(){
  int i2=222;
  p2 = &i2;
  cout << *p2 <<endl;

It does make a difference if you return a ptr-to-const. The caller would make a clone of the ptr-to-const and can’t subsequently write to the pointee.

4) How about returning a const Trade object? [1] gave some justifications:

[1] https://www.linuxtopia.org/online_books/programming_books/thinking_in_c++/Chapter08_014.html

vector initializer: disallowed in field declaration until c++11

in a field declaration, you face restrictions:

vector<int> vec; //acceptable
vector<int> vec(9); // field initialization, disallowed until C++11. See https://stackoverflow.com/questions/24149924/has-the-new-c11-member-initialization-feature-at-declaration-made-initializati

You can write this kinda code in a *local* variable definition though.

c++no-arg ctor Call +! q[new] #confusing

(Any QQ value? Yes some crazy interviewers may ask.)

With “new” or with ctor arguments, the confusion is lower. Note since no “new” in this entire article, all the instances are constructed on stack or global area.

Compare to java, the c++ default construction syntax looks confusing even inconsistent —

   Account* addr = &Account() //instantiates temp.. and saves its address. Not sure if this is meaningful.

Note  Account() is parsed differently by compiler, depending on context. In each case below, Account() looks like a function call returning an Account on stack–
   Account() ; // on a line by itself this expression simply creates a local temp instance on stack then …DISCARDED.
Account().print(); // instantiates temp, then invokes a method

Account myAcct = Account(); //(tested) unpopular but rather consistent with new(), and consistent with other languages. Arguably Recommended to beginners though inefficient. http://stackoverflow.com/questions/11480545/explicit-copy-constructor explains what it does – 

{create a temporary Account on the rhs; Implicitly call the copy constructor to copy from that temporary into myAcct.Marking the copy ctor “explicit” would break this code, which is recommended by many authors.

    ifstream in = ifstream(“aaa.txt”); //broken – due to deleted copy ctor
    ifstream in(“aaa.txt”); //tested. P96[[essentialC++]]

    Account arr[] = {Account(),Account()}; //2 ctor calls. Actually, You can even pass args to initialize the two array elements.

   func1( Account() ); //creates a temp and pass it to func1(). Trailing parens are necessary. I feel this is practical and common, but I seldom notice it.
   throw Account(); //creates a temp and.. See P1035 [[c++Primer]]
   count_if(itr1 , itr2 , MyPredicateFunctor()); //creates a temp … See P107 [[DanielDuffy]] + P86[[essentialC++]]

std::string const var; // no parentheses … allocates a const instance
class MyClass{/*empty class*/};
MyClass var3; // ok allocates the object behind the variable without initializing it
MyClass const var2; // can’t compile but why?
Answer — You need to add empty no-arg ctor into MyClass to avoid a POD class.
(See the code below.) I personally prefer the Account(…) syntax above, and avoid #5/#6 below, but #5/#6 are popular. We just have to memorize the inconsistent syntax about them. #6 is clear and unambiguous. #9 seems to follow the #6 syntax “pattern” but alas! The correct way to achieve the purpose is #5.

    Account myAcct; // #5 declares a variable and allocates an Account object. Popular idiom, but somewhat inconsistent with other syntax constructs like the (123) caseTrailing parens are problematic and will lead to #9:
    Account myAcct(); // #9 NOT a ctor call; declares a function! Anything parseable as a func declaration is parsed that way. See P34 [[effSTL]]

     Account myAcct(123); // #6 initialize the myAcct variable with a stack object,  not  discarded.

avoid calling wait() while holding multiple locks

Avoid calling wait() while holding 2 locks. I always try very hard to call lock1.wait() while holding nothing but lock1. Any lock2 held would NOT be released by lock1.wait().

Similarly, avoid grabbing a lock while holding other locks.

Beware of all blocking operations like lock-grabbing, wait(), while loop, join() and sleep(longPeriod). If the blocked thread is the only thread capable of “releasing” some shared resource, then that resource gets locked up along with the thread, starving other threads who needs it.

In contrast, tryLock(), wait(shortPerod), join(shortPeriod) are safer.

when Must RHS be a Lvalue expression


Q: when must the RHS be a L-value expression?
%%A: it must if LHS is a non-const reference variable, as the var must bind a Location. This applies to function arguments, too.

C++ true-blue call-by-reference requires the argument (i.e. RHS) be an L-value. RHS can’t be a literal “3” or a math expression. An L-value has an address so can be passed by reference.

Q: can RHS be a new-expression? Does it qualify as a RHS L-value?
A: YES tested — qq/ *(new int(8)) /, presumably because this object has an address

Q: what if the LHS is a ref-to-const?
A: then RHS can be literals like “3”. Tested.

Q: how about a function call returning an object? Can this be an RHS L-value?
A: YES if the function returns by reference. Tested. Such a function-call IS a true-blue L-value-expression.
A: NO if the function returns by value. Such a function-call ISN’T an L-value-expression.

pbref between const levels #const radiates left

When you pass an arg  into a function param by reference, check the const-ness of LHS vs RHS. This post is about when they differ const-wise. Note the most common and tricky situation is func pbref (ie pass-by-reference). A programmer needs to recognize them without thinking. The assignment operations below are less common but simpler.

Note — in pbref, on LHS we sometimes create a completely new ref (4-byte) with(out) const-ness. This ref has its (unknowable) new address. This address is different from address of the RHS (which must be a lval — u can’t do int& r = 4444).

I feel a const stackVar is a const object. There’s no other way to get at the object, since the const var blocks all [3] edits.

Q: Given f(const int & i), can you pass in an arg of non-const int variable?
A: yes. Const LHS is tolerant and extremely widespread[1]. The pbref process adds constness to the new LHS reference. Equivalent to the simpler but less common

int arg = 9;
const int & a = arg;

Q: Remove const — Given f(int & i), can you pass in a const int variable?
A: illegal. The arg variable is const, so it promises not to modify state. The pbref process attempts to remove the constness. Equivalent to the simpler but less common

const int arg = 9;
int & a = arg; // illegal

In summary, Const-ness radiates from RHS to LHS. Illegal to block it.
* If RHS is a const ptr to a mutable object, then that constness doesn’t radiate left-ward
* If RHS is any handle[2] on a const object, then yes that constness radiates left-ward.
* If RHS is a handle on a mutable object, then LHS can be anything.

sound byte– if you have only a handle on a const object, then you can’t [3] modify its state even if you copy that handle. Object is not editable *via* this handle.

sound byte — If a (method or) variable is declared without “const”, compiler assumes this “handle” (can and therefore) will modify object state.

Compiler disallows assigning a const RHS handle to a mutable LHS handle. “Handle” is typically a ref. (For a ptr, “const handle” means ptr-to-const). However, if you deep-copy a target object (not the 4-byte address), then const-ness doesn’t radiate left. You can make a mutable deep copy from a const object, without compromising const-ness of the RHS object. All subsequent “edits” happen on the deep copy.

Tricky context — Constness still radiates leftward upon method-calling, because we perform a

implicit_this_param = & handleObj; //LHS is const param IFF a const method

  • constObj.constMethod() // good
  • constObj.mutableMethod() // won’t compile
  • mutableObj.constMethod() //good

Q: need to explicitly remove constness of a q(const char *), which is a c-string
A: Yes. Constness radiates left. Use const_cast. See post on const char *

[1] copiers and assignments usually have const ref params.
[2] handle can mean a nonref variable or a ref/ptr
[3] except via explicit const_cast

(below is a piece of work in progress….)
Now we can better understand why auto_ptr is bad for containers. Given an auto_ptr object A. A’s copier and assignment both take a non-const RHS. Container functions need copier with const param ie a const LHS, but
* can they enforce that copier LHS be declared const? No. If you could then auto_ptr + container would have been illegal combination??
* can they enforce that copier RHS be declared const? No compiler can’t enforce it — const radiates left. Compiler can only enforce LHS to be const.

pure-pure virtual invoked during base ctor/dtor #bbg

Update: P273 [[eff c#]] compares c# vs c++ when a pure virtual is invoked by ctor (book skips dtor). It confirms c++ would crash, but my bloodshed c++ compiler detected pure virtual called in base ctor and failed compilation. See source code at bottom.

This is rather obscure , not typical. Not even a valid question.

struct B{
 virtual void cleanup() = 0;
  }// not virtual
struct D: public B{
 void cleanup(){}
int main(){ D derivedObject; } 
// derivedObject destructed. 
// If (see below why impossible) code were to compile, what would happen here?

%%A: the invisible ~D() runs, then the defined ~B() runs even though it’s non-virtual.
%%A: I think it’s undefined behavior ONLY with “delete”.
%%A: virtual method called during base object destruction/construction – Warning by Scott Meyers. At time of base class destruction only the pure virtual is available, so system crashes saying “pure virtual function called”.

(In java, superclass ctor calling a virtual function results in the subclass’s version invoked, before the subclass ctor runs! Compiles but dangerous. If you do this, make sure the subclass ctor is empty.)

Q: how can a compiler intercept that error condition, with 0 runtime cost?
A: see post on pure pure virtual

Note, if Derived had not implemented the pure virtual, then Derived would have become an abstract class and non-instantiable.

Actually compiler detects the call to cleanup() is a call to B::cleanup() which is abstract. Here’s a demo.

struct B{
  virtual void cleanup() = 0;
  ~B();  // not virtual
//void B::cleanup(){       cout<<"B::cleanup\n";}
  cout<<"B()\n"; //this->cleanup(); //breaks compiler if enabled
  cout<<"~B()\n"; //this->cleanup(); //breaks compiler if enabled
struct D: public B{
  void cleanup(){       cout<<"D::cleanup\n"; }
  ~D(){cout<<"~D()\n"; }
int main(){
    if (true){
       D derivedObject; // derivedObject destructed. Suppose this can compile, what will happen?